diff --git a/epd/epd.go b/epd/epd.go index 9472624..9d7670d 100644 --- a/epd/epd.go +++ b/epd/epd.go @@ -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 } diff --git a/icons/01d.png b/icons/01d.png new file mode 100644 index 0000000..85efa16 Binary files /dev/null and b/icons/01d.png differ diff --git a/icons/02d.png b/icons/02d.png new file mode 100644 index 0000000..288a40e Binary files /dev/null and b/icons/02d.png differ diff --git a/icons/03d.png b/icons/03d.png new file mode 100644 index 0000000..ef2e9f7 Binary files /dev/null and b/icons/03d.png differ diff --git a/icons/04d.png b/icons/04d.png new file mode 100644 index 0000000..9c64ea8 Binary files /dev/null and b/icons/04d.png differ diff --git a/icons/09d.png b/icons/09d.png new file mode 100644 index 0000000..0f14cb6 Binary files /dev/null and b/icons/09d.png differ diff --git a/icons/10d.png b/icons/10d.png new file mode 100644 index 0000000..b5e5d10 Binary files /dev/null and b/icons/10d.png differ diff --git a/icons/11d.png b/icons/11d.png new file mode 100644 index 0000000..4a885cf Binary files /dev/null and b/icons/11d.png differ diff --git a/icons/13d.png b/icons/13d.png new file mode 100644 index 0000000..7867322 Binary files /dev/null and b/icons/13d.png differ diff --git a/icons/50d.png b/icons/50d.png new file mode 100644 index 0000000..f04122b Binary files /dev/null and b/icons/50d.png differ diff --git a/img.go b/img.go index 66f9891..0bd9b4d 100644 --- a/img.go +++ b/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 + } + + 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 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) +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 { diff --git a/main.go b/main.go index 8b5a25b..0eada30 100644 --- a/main.go +++ b/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) } diff --git a/run_darwin_arm64.go b/run_darwin_arm64.go index ce0fb5a..249a9d0 100644 --- a/run_darwin_arm64.go +++ b/run_darwin_arm64.go @@ -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 } diff --git a/run_linux_arm64.go b/run_linux_arm64.go index a0e7e9b..658aea5 100644 --- a/run_linux_arm64.go +++ b/run_linux_arm64.go @@ -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) } diff --git a/weather/weather.go b/weather/weather.go new file mode 100644 index 0000000..ff36a96 --- /dev/null +++ b/weather/weather.go @@ -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 +}