display weather & resize
|
@ -317,5 +317,5 @@ func (e *EPD) sendImg(img image.Image) {
|
|||
}
|
||||
|
||||
func isdark(r, g, b, _ uint32) bool {
|
||||
return r < 255 || g < 255 || b < 255
|
||||
return r < 65535 || g < 65535 || b < 65535
|
||||
}
|
||||
|
|
BIN
icons/01d.png
Normal file
After Width: | Height: | Size: 945 B |
BIN
icons/02d.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/03d.png
Normal file
After Width: | Height: | Size: 837 B |
BIN
icons/04d.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/09d.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
icons/10d.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
icons/11d.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
icons/13d.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
icons/50d.png
Normal file
After Width: | Height: | Size: 650 B |
152
img.go
|
@ -2,43 +2,52 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/Crocmagnon/display-epaper/epd"
|
||||
"github.com/Crocmagnon/display-epaper/fete"
|
||||
"github.com/Crocmagnon/display-epaper/transports"
|
||||
"github.com/Crocmagnon/display-epaper/weather"
|
||||
"github.com/llgcode/draw2d"
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getBlack(
|
||||
ctx context.Context,
|
||||
nowFunc func() time.Time,
|
||||
transportsClient *transports.Client,
|
||||
feteClient *fete.Client,
|
||||
) (*image.RGBA, error) {
|
||||
//go:embed icons
|
||||
var icons embed.FS
|
||||
|
||||
const (
|
||||
leftX = 20
|
||||
rightX = 530
|
||||
)
|
||||
|
||||
func getBlack(ctx context.Context, nowFunc func() time.Time, transportsClient *transports.Client, feteClient *fete.Client, weatherClient *weather.Client) (*image.RGBA, error) {
|
||||
bus, err := transportsClient.GetTCLPassages(ctx, 290)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting bus: %w", err)
|
||||
log.Println("error getting bus:", err)
|
||||
}
|
||||
tram, err := transportsClient.GetTCLPassages(ctx, 34068)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting tram: %w", err)
|
||||
log.Println("error getting tram:", err)
|
||||
}
|
||||
velovRoc, err := transportsClient.GetVelovStation(ctx, 10044)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting velov: %w", err)
|
||||
log.Println("error getting velov:", err)
|
||||
}
|
||||
|
||||
fetes, err := feteClient.GetFete(ctx, nowFunc())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting fetes: %w", err)
|
||||
log.Println("error getting fetes:", err)
|
||||
}
|
||||
wthr, err := weatherClient.GetWeather(ctx)
|
||||
if err != nil {
|
||||
log.Println("error getting weather:", err)
|
||||
}
|
||||
|
||||
_ = fetes
|
||||
|
||||
img := newWhite()
|
||||
|
||||
|
@ -48,35 +57,93 @@ func getBlack(
|
|||
gc.SetFillColor(color.RGBA{255, 255, 255, 255})
|
||||
gc.SetStrokeColor(color.RGBA{0, 0, 0, 255})
|
||||
|
||||
rect(gc, 0, 0, 800, 480)
|
||||
|
||||
drawTCL(gc, bus, 30)
|
||||
drawTCL(gc, tram, 180)
|
||||
drawTCL(gc, bus, 55)
|
||||
drawTCL(gc, tram, 190)
|
||||
drawVelov(gc, velovRoc, 350)
|
||||
drawDateFete(gc, fetes, nowFunc())
|
||||
drawDate(gc, nowFunc())
|
||||
drawFete(gc, fetes)
|
||||
drawWeather(gc, wthr)
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func drawVelov(gc *draw2dimg.GraphicContext, station *transports.Station, yOffset float64) {
|
||||
x := float64(600)
|
||||
text(gc, station.Name, 15, x, yOffset)
|
||||
text(gc, fmt.Sprintf("V : %v - P : %v", station.BikesAvailable, station.DocksAvailable), 15, x, yOffset+30)
|
||||
func drawWeather(gc *draw2dimg.GraphicContext, wthr *weather.Prevision) {
|
||||
if wthr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
func drawDateFete(gc *draw2dimg.GraphicContext, fetes *fete.Fete, now time.Time) {
|
||||
text(gc, now.Format("15:04"), 40, 20, 190)
|
||||
text(gc, getDate(now), 50, 20, 255)
|
||||
text(gc, fmt.Sprintf("On fête les %s", fetes.Name), 17, 20, 400)
|
||||
if len(wthr.Daily) == 0 || len(wthr.Daily[0].Weather) == 0 {
|
||||
log.Println("missing daily or daily weather")
|
||||
return
|
||||
}
|
||||
|
||||
daily := wthr.Daily[0]
|
||||
dailyWeather := daily.Weather[0]
|
||||
err := drawWeatherIcon(gc, dailyWeather)
|
||||
if err != nil {
|
||||
log.Println("Failed to draw weather icon:", err)
|
||||
}
|
||||
|
||||
text(gc, formatTemp("Act", wthr.Current.Temp), 18, 100, 45)
|
||||
text(gc, formatTemp("Max", daily.Temp.Max), 18, 220, 45)
|
||||
text(gc, fmt.Sprintf("Pluie : %v%%", int(math.Round(daily.Pop))), 18, 100, 75)
|
||||
text(gc, dailyWeather.Description, 18, leftX, 110)
|
||||
}
|
||||
|
||||
func drawWeatherIcon(gc *draw2dimg.GraphicContext, dailyWeather weather.Weather) error {
|
||||
icon := strings.TrimSuffix(dailyWeather.Icon, "d")
|
||||
icon = strings.TrimSuffix(icon, "n")
|
||||
f, err := icons.Open(fmt.Sprintf("icons/%sd.png", icon))
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening icon: %w", err)
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding icon: %w", err)
|
||||
}
|
||||
|
||||
gc.DrawImage(img)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatTemp(name string, temp float64) string {
|
||||
return fmt.Sprintf("%v : %v°C", name, int(math.Round(temp)))
|
||||
}
|
||||
|
||||
func drawVelov(gc *draw2dimg.GraphicContext, station *transports.Station, yOffset float64) {
|
||||
if station == nil {
|
||||
return
|
||||
}
|
||||
|
||||
text(gc, station.Name, 23, rightX, yOffset)
|
||||
text(gc, fmt.Sprintf("V : %v - P : %v", station.BikesAvailable, station.DocksAvailable), 23, rightX, yOffset+30)
|
||||
}
|
||||
|
||||
func drawDate(gc *draw2dimg.GraphicContext, now time.Time) {
|
||||
text(gc, now.Format("15:04"), 60, leftX, 240)
|
||||
text(gc, getDate(now), 30, leftX, 285)
|
||||
}
|
||||
|
||||
func drawFete(gc *draw2dimg.GraphicContext, fetes *fete.Fete) {
|
||||
if fetes == nil {
|
||||
return
|
||||
}
|
||||
|
||||
text(gc, fmt.Sprintf("On fête les %s", fetes.Name), 18, leftX, 400)
|
||||
}
|
||||
|
||||
func drawTCL(gc *draw2dimg.GraphicContext, passages *transports.Passages, yoffset float64) {
|
||||
if passages == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for i, passage := range passages.Passages {
|
||||
x := float64(600 + i*100)
|
||||
text(gc, passage.Ligne, 15, x, yoffset)
|
||||
x := float64(rightX + i*120)
|
||||
text(gc, passage.Ligne, 23, x, yoffset)
|
||||
for j, delay := range passage.Delays {
|
||||
y := yoffset + float64(j+1)*30
|
||||
text(gc, delay, 15, x, y)
|
||||
y := yoffset + float64(j+1)*35
|
||||
text(gc, delay, 23, x, y)
|
||||
if j >= 2 { // limit number of delays displayed
|
||||
break
|
||||
}
|
||||
|
@ -89,8 +156,6 @@ func text(gc *draw2dimg.GraphicContext, s string, size, x, y float64) {
|
|||
gc.SetFontData(draw2d.FontData{Name: fontName})
|
||||
gc.SetFontSize(size)
|
||||
gc.FillStringAt(s, x, y)
|
||||
gc.SetLineWidth(2)
|
||||
gc.StrokeStringAt(s, x, y)
|
||||
}
|
||||
|
||||
func newWhite() *image.RGBA {
|
||||
|
@ -114,7 +179,28 @@ func rect(gc *draw2dimg.GraphicContext, x1, y1, x2, y2 float64) {
|
|||
}
|
||||
|
||||
func getDate(now time.Time) string {
|
||||
return fmt.Sprintf("%v %v", getDay(now), getMonth(now))
|
||||
return fmt.Sprintf("%v %v %v", getDow(now), getDay(now), getMonth(now))
|
||||
}
|
||||
|
||||
func getDow(now time.Time) string {
|
||||
switch now.Weekday() {
|
||||
case time.Monday:
|
||||
return "lun"
|
||||
case time.Tuesday:
|
||||
return "mar"
|
||||
case time.Wednesday:
|
||||
return "mer"
|
||||
case time.Thursday:
|
||||
return "jeu"
|
||||
case time.Friday:
|
||||
return "ven"
|
||||
case time.Saturday:
|
||||
return "sam"
|
||||
case time.Sunday:
|
||||
return "dim"
|
||||
}
|
||||
|
||||
return "?"
|
||||
}
|
||||
|
||||
func getDay(now time.Time) string {
|
||||
|
|
7
main.go
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"github.com/Crocmagnon/display-epaper/fete"
|
||||
"github.com/Crocmagnon/display-epaper/transports"
|
||||
"github.com/Crocmagnon/display-epaper/weather"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/llgcode/draw2d"
|
||||
_ "golang.org/x/image/bmp"
|
||||
|
@ -34,7 +35,11 @@ func main() {
|
|||
CacheLocation: os.Getenv("FETE_CACHE_LOCATION"),
|
||||
})
|
||||
|
||||
if err := run(ctx, transportsClient, feteClient); err != nil {
|
||||
weatherClient := weather.New(nil, weather.Config{
|
||||
APIKey: os.Getenv("WEATHER_API_KEY"),
|
||||
})
|
||||
|
||||
if err := run(ctx, transportsClient, feteClient, weatherClient); err != nil {
|
||||
log.Fatal("error: ", err)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,19 +4,25 @@ import (
|
|||
"context"
|
||||
"github.com/Crocmagnon/display-epaper/fete"
|
||||
"github.com/Crocmagnon/display-epaper/transports"
|
||||
"github.com/Crocmagnon/display-epaper/weather"
|
||||
"github.com/llgcode/draw2d/draw2dimg"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
func run(ctx context.Context, transportsClient *transports.Client, feteClient *fete.Client) error {
|
||||
func run(
|
||||
ctx context.Context,
|
||||
transportsClient *transports.Client,
|
||||
feteClient *fete.Client,
|
||||
weatherClient *weather.Client,
|
||||
) error {
|
||||
img, err := getBlack(ctx, func() time.Time {
|
||||
t, err := time.Parse(time.DateOnly, "2024-08-01zzz")
|
||||
if err != nil {
|
||||
return time.Now()
|
||||
}
|
||||
return t
|
||||
}, transportsClient, feteClient)
|
||||
}, transportsClient, feteClient, weatherClient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -24,7 +30,5 @@ func run(ctx context.Context, transportsClient *transports.Client, feteClient *f
|
|||
log.Fatalf("error saving image: %v", err)
|
||||
}
|
||||
|
||||
log.Println("done")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,12 +6,18 @@ import (
|
|||
"github.com/Crocmagnon/display-epaper/epd"
|
||||
"github.com/Crocmagnon/display-epaper/fete"
|
||||
"github.com/Crocmagnon/display-epaper/transports"
|
||||
"github.com/Crocmagnon/display-epaper/weather"
|
||||
"log"
|
||||
"periph.io/x/host/v3"
|
||||
"time"
|
||||
)
|
||||
|
||||
func run(ctx context.Context, transportsClient *transports.Client, feteClient *fete.Client) error {
|
||||
func run(
|
||||
ctx context.Context,
|
||||
transportsClient *transports.Client,
|
||||
feteClient *fete.Client,
|
||||
weatherClient *weather.Client,
|
||||
) error {
|
||||
_, err := host.Init()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing host: %w", err)
|
||||
|
@ -30,7 +36,7 @@ func run(ctx context.Context, transportsClient *transports.Client, feteClient *f
|
|||
default:
|
||||
}
|
||||
|
||||
err = loop(ctx, display, transportsClient, feteClient)
|
||||
err = loop(ctx, display, transportsClient, feteClient, weatherClient)
|
||||
if err != nil {
|
||||
log.Printf("error looping: %v\n", err)
|
||||
}
|
||||
|
@ -45,6 +51,7 @@ func loop(
|
|||
display *epd.EPD,
|
||||
transportsClient *transports.Client,
|
||||
feteClient *fete.Client,
|
||||
weatherClient *weather.Client,
|
||||
) error {
|
||||
defer func() {
|
||||
if err := display.Sleep(); err != nil {
|
||||
|
@ -59,7 +66,7 @@ func loop(
|
|||
|
||||
display.Clear()
|
||||
|
||||
black, err := getBlack(ctx, time.Now, transportsClient, feteClient)
|
||||
black, err := getBlack(ctx, time.Now, transportsClient, feteClient, weatherClient)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting black: %w", err)
|
||||
}
|
||||
|
|
122
weather/weather.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package weather
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/carlmjohnson/requests"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
config Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func New(httpClient *http.Client, config Config) *Client {
|
||||
return &Client{
|
||||
config: config,
|
||||
client: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
type Prevision struct {
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
Timezone string `json:"timezone"`
|
||||
TimezoneOffset int `json:"timezone_offset"`
|
||||
Current struct {
|
||||
Dt int `json:"dt"`
|
||||
Sunrise int `json:"sunrise"`
|
||||
Sunset int `json:"sunset"`
|
||||
Temp float64 `json:"temp"`
|
||||
FeelsLike float64 `json:"feels_like"`
|
||||
Pressure int `json:"pressure"`
|
||||
Humidity int `json:"humidity"`
|
||||
DewPoint float64 `json:"dew_point"`
|
||||
Uvi float64 `json:"uvi"`
|
||||
Clouds int `json:"clouds"`
|
||||
Visibility int `json:"visibility"`
|
||||
WindSpeed float64 `json:"wind_speed"`
|
||||
WindDeg int `json:"wind_deg"`
|
||||
WindGust float64 `json:"wind_gust"`
|
||||
Weather []struct {
|
||||
Id int `json:"id"`
|
||||
Main string `json:"main"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
} `json:"weather"`
|
||||
} `json:"current"`
|
||||
Daily []Daily `json:"daily"`
|
||||
Alerts []struct {
|
||||
SenderName string `json:"sender_name"`
|
||||
Event string `json:"event"`
|
||||
Start int `json:"start"`
|
||||
End int `json:"end"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
} `json:"alerts"`
|
||||
}
|
||||
|
||||
type Daily struct {
|
||||
Dt int `json:"dt"`
|
||||
Sunrise int `json:"sunrise"`
|
||||
Sunset int `json:"sunset"`
|
||||
Moonrise int `json:"moonrise"`
|
||||
Moonset int `json:"moonset"`
|
||||
MoonPhase float64 `json:"moon_phase"`
|
||||
Summary string `json:"summary"`
|
||||
Temp struct {
|
||||
Day float64 `json:"day"`
|
||||
Min float64 `json:"min"`
|
||||
Max float64 `json:"max"`
|
||||
Night float64 `json:"night"`
|
||||
Eve float64 `json:"eve"`
|
||||
Morn float64 `json:"morn"`
|
||||
} `json:"temp"`
|
||||
FeelsLike struct {
|
||||
Day float64 `json:"day"`
|
||||
Night float64 `json:"night"`
|
||||
Eve float64 `json:"eve"`
|
||||
Morn float64 `json:"morn"`
|
||||
} `json:"feels_like"`
|
||||
Pressure int `json:"pressure"`
|
||||
Humidity int `json:"humidity"`
|
||||
DewPoint float64 `json:"dew_point"`
|
||||
WindSpeed float64 `json:"wind_speed"`
|
||||
WindDeg int `json:"wind_deg"`
|
||||
WindGust float64 `json:"wind_gust"`
|
||||
Weather []Weather `json:"weather"`
|
||||
Clouds int `json:"clouds"`
|
||||
Pop float64 `json:"pop"`
|
||||
Rain float64 `json:"rain"`
|
||||
Uvi float64 `json:"uvi"`
|
||||
}
|
||||
|
||||
type Weather struct {
|
||||
Id int `json:"id"`
|
||||
Main string `json:"main"`
|
||||
Description string `json:"description"`
|
||||
Icon string `json:"icon"`
|
||||
}
|
||||
|
||||
func (c *Client) GetWeather(ctx context.Context) (res *Prevision, err error) {
|
||||
err = requests.URL("https://api.openweathermap.org/data/3.0/onecall").
|
||||
Client(c.client).
|
||||
Param("lat", "45.78").
|
||||
Param("lon", "4.89").
|
||||
Param("appid", c.config.APIKey).
|
||||
Param("units", "metric").
|
||||
Param("lang", "fr").
|
||||
Param("exclude", "minutely,hourly").
|
||||
ToJSON(&res).
|
||||
Fetch(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("calling openweathermap: %w", err)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|