display weather & resize

This commit is contained in:
Gabriel Augendre 2024-09-15 23:25:17 +02:00
parent b954904954
commit 567f7517f9
15 changed files with 266 additions and 42 deletions

View file

@ -317,5 +317,5 @@ func (e *EPD) sendImg(img image.Image) {
} }
func isdark(r, g, b, _ uint32) bool { 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

BIN
icons/02d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
icons/03d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

BIN
icons/04d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
icons/09d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
icons/10d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
icons/11d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
icons/13d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
icons/50d.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

152
img.go
View file

@ -2,43 +2,52 @@ package main
import ( import (
"context" "context"
"embed"
"fmt" "fmt"
"github.com/Crocmagnon/display-epaper/epd" "github.com/Crocmagnon/display-epaper/epd"
"github.com/Crocmagnon/display-epaper/fete" "github.com/Crocmagnon/display-epaper/fete"
"github.com/Crocmagnon/display-epaper/transports" "github.com/Crocmagnon/display-epaper/transports"
"github.com/Crocmagnon/display-epaper/weather"
"github.com/llgcode/draw2d" "github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg" "github.com/llgcode/draw2d/draw2dimg"
"image" "image"
"image/color" "image/color"
"log"
"math"
"strconv" "strconv"
"strings"
"time" "time"
) )
func getBlack( //go:embed icons
ctx context.Context, var icons embed.FS
nowFunc func() time.Time,
transportsClient *transports.Client, const (
feteClient *fete.Client, leftX = 20
) (*image.RGBA, error) { 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) bus, err := transportsClient.GetTCLPassages(ctx, 290)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting bus: %w", err) log.Println("error getting bus:", err)
} }
tram, err := transportsClient.GetTCLPassages(ctx, 34068) tram, err := transportsClient.GetTCLPassages(ctx, 34068)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting tram: %w", err) log.Println("error getting tram:", err)
} }
velovRoc, err := transportsClient.GetVelovStation(ctx, 10044) velovRoc, err := transportsClient.GetVelovStation(ctx, 10044)
if err != nil { if err != nil {
return nil, fmt.Errorf("getting velov: %w", err) log.Println("error getting velov:", err)
} }
fetes, err := feteClient.GetFete(ctx, nowFunc()) fetes, err := feteClient.GetFete(ctx, nowFunc())
if err != nil { 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() img := newWhite()
@ -48,35 +57,93 @@ func getBlack(
gc.SetFillColor(color.RGBA{255, 255, 255, 255}) gc.SetFillColor(color.RGBA{255, 255, 255, 255})
gc.SetStrokeColor(color.RGBA{0, 0, 0, 255}) gc.SetStrokeColor(color.RGBA{0, 0, 0, 255})
rect(gc, 0, 0, 800, 480) drawTCL(gc, bus, 55)
drawTCL(gc, tram, 190)
drawTCL(gc, bus, 30)
drawTCL(gc, tram, 180)
drawVelov(gc, velovRoc, 350) drawVelov(gc, velovRoc, 350)
drawDateFete(gc, fetes, nowFunc()) drawDate(gc, nowFunc())
drawFete(gc, fetes)
drawWeather(gc, wthr)
return img, nil return img, nil
} }
func drawVelov(gc *draw2dimg.GraphicContext, station *transports.Station, yOffset float64) { func drawWeather(gc *draw2dimg.GraphicContext, wthr *weather.Prevision) {
x := float64(600) if wthr == nil {
text(gc, station.Name, 15, x, yOffset) return
text(gc, fmt.Sprintf("V : %v - P : %v", station.BikesAvailable, station.DocksAvailable), 15, x, yOffset+30)
} }
func drawDateFete(gc *draw2dimg.GraphicContext, fetes *fete.Fete, now time.Time) { if len(wthr.Daily) == 0 || len(wthr.Daily[0].Weather) == 0 {
text(gc, now.Format("15:04"), 40, 20, 190) log.Println("missing daily or daily weather")
text(gc, getDate(now), 50, 20, 255) return
text(gc, fmt.Sprintf("On fête les %s", fetes.Name), 17, 20, 400) }
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) { func drawTCL(gc *draw2dimg.GraphicContext, passages *transports.Passages, yoffset float64) {
if passages == nil {
return
}
for i, passage := range passages.Passages { for i, passage := range passages.Passages {
x := float64(600 + i*100) x := float64(rightX + i*120)
text(gc, passage.Ligne, 15, x, yoffset) text(gc, passage.Ligne, 23, x, yoffset)
for j, delay := range passage.Delays { for j, delay := range passage.Delays {
y := yoffset + float64(j+1)*30 y := yoffset + float64(j+1)*35
text(gc, delay, 15, x, y) text(gc, delay, 23, x, y)
if j >= 2 { // limit number of delays displayed if j >= 2 { // limit number of delays displayed
break break
} }
@ -89,8 +156,6 @@ func text(gc *draw2dimg.GraphicContext, s string, size, x, y float64) {
gc.SetFontData(draw2d.FontData{Name: fontName}) gc.SetFontData(draw2d.FontData{Name: fontName})
gc.SetFontSize(size) gc.SetFontSize(size)
gc.FillStringAt(s, x, y) gc.FillStringAt(s, x, y)
gc.SetLineWidth(2)
gc.StrokeStringAt(s, x, y)
} }
func newWhite() *image.RGBA { func newWhite() *image.RGBA {
@ -114,7 +179,28 @@ func rect(gc *draw2dimg.GraphicContext, x1, y1, x2, y2 float64) {
} }
func getDate(now time.Time) string { 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 { func getDay(now time.Time) string {

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/Crocmagnon/display-epaper/fete" "github.com/Crocmagnon/display-epaper/fete"
"github.com/Crocmagnon/display-epaper/transports" "github.com/Crocmagnon/display-epaper/transports"
"github.com/Crocmagnon/display-epaper/weather"
"github.com/golang/freetype/truetype" "github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d" "github.com/llgcode/draw2d"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
@ -34,7 +35,11 @@ func main() {
CacheLocation: os.Getenv("FETE_CACHE_LOCATION"), 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) log.Fatal("error: ", err)
} }

View file

@ -4,19 +4,25 @@ import (
"context" "context"
"github.com/Crocmagnon/display-epaper/fete" "github.com/Crocmagnon/display-epaper/fete"
"github.com/Crocmagnon/display-epaper/transports" "github.com/Crocmagnon/display-epaper/transports"
"github.com/Crocmagnon/display-epaper/weather"
"github.com/llgcode/draw2d/draw2dimg" "github.com/llgcode/draw2d/draw2dimg"
"log" "log"
"time" "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 { img, err := getBlack(ctx, func() time.Time {
t, err := time.Parse(time.DateOnly, "2024-08-01zzz") t, err := time.Parse(time.DateOnly, "2024-08-01zzz")
if err != nil { if err != nil {
return time.Now() return time.Now()
} }
return t return t
}, transportsClient, feteClient) }, transportsClient, feteClient, weatherClient)
if err != nil { if err != nil {
log.Fatal(err) 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.Fatalf("error saving image: %v", err)
} }
log.Println("done")
return nil return nil
} }

View file

@ -6,12 +6,18 @@ import (
"github.com/Crocmagnon/display-epaper/epd" "github.com/Crocmagnon/display-epaper/epd"
"github.com/Crocmagnon/display-epaper/fete" "github.com/Crocmagnon/display-epaper/fete"
"github.com/Crocmagnon/display-epaper/transports" "github.com/Crocmagnon/display-epaper/transports"
"github.com/Crocmagnon/display-epaper/weather"
"log" "log"
"periph.io/x/host/v3" "periph.io/x/host/v3"
"time" "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() _, err := host.Init()
if err != nil { if err != nil {
return fmt.Errorf("initializing host: %w", err) return fmt.Errorf("initializing host: %w", err)
@ -30,7 +36,7 @@ func run(ctx context.Context, transportsClient *transports.Client, feteClient *f
default: default:
} }
err = loop(ctx, display, transportsClient, feteClient) err = loop(ctx, display, transportsClient, feteClient, weatherClient)
if err != nil { if err != nil {
log.Printf("error looping: %v\n", err) log.Printf("error looping: %v\n", err)
} }
@ -45,6 +51,7 @@ func loop(
display *epd.EPD, display *epd.EPD,
transportsClient *transports.Client, transportsClient *transports.Client,
feteClient *fete.Client, feteClient *fete.Client,
weatherClient *weather.Client,
) error { ) error {
defer func() { defer func() {
if err := display.Sleep(); err != nil { if err := display.Sleep(); err != nil {
@ -59,7 +66,7 @@ func loop(
display.Clear() display.Clear()
black, err := getBlack(ctx, time.Now, transportsClient, feteClient) black, err := getBlack(ctx, time.Now, transportsClient, feteClient, weatherClient)
if err != nil { if err != nil {
return fmt.Errorf("getting black: %w", err) return fmt.Errorf("getting black: %w", err)
} }

122
weather/weather.go Normal file
View 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
}