display-epaper/img.go

412 lines
10 KiB
Go
Raw Normal View History

2024-09-15 09:01:13 +02:00
package main
import (
2024-09-15 10:46:07 +02:00
"context"
2024-09-15 23:25:17 +02:00
"embed"
2024-09-15 10:46:07 +02:00
"fmt"
2024-09-15 09:01:13 +02:00
"github.com/Crocmagnon/display-epaper/epd"
2024-11-26 02:34:53 +01:00
"github.com/Crocmagnon/display-epaper/fonts"
2024-09-18 22:26:40 +02:00
"github.com/Crocmagnon/display-epaper/home_assistant"
2024-09-15 23:25:17 +02:00
"github.com/Crocmagnon/display-epaper/weather"
2024-09-15 09:01:13 +02:00
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"image"
"image/color"
"log/slog"
2024-09-15 23:25:17 +02:00
"math"
2024-09-15 12:00:36 +02:00
"strconv"
2024-09-15 23:25:17 +02:00
"strings"
2024-09-16 14:20:59 +02:00
"sync"
2024-09-15 12:00:36 +02:00
"time"
2024-09-15 09:01:13 +02:00
)
2024-09-15 23:25:17 +02:00
//go:embed icons
var icons embed.FS
const (
leftX = 20
rightX = 530
)
func getImg(ctx context.Context, nowFunc func() time.Time, weatherClient *weather.Client, hassClient *home_assistant.Client) (*image.RGBA, error) {
2024-09-16 14:20:59 +02:00
var (
2024-12-16 23:25:45 +01:00
bus37 []time.Time
busC17 []time.Time
tramT1 []time.Time
velovRocBikes string
velovRocStands string
feteName string
wthr *weather.Prevision
msg string
2024-09-16 14:20:59 +02:00
)
wg := &sync.WaitGroup{}
wg.Add(5)
2024-09-16 14:20:59 +02:00
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
2024-09-16 14:20:59 +02:00
defer cancel()
2024-12-16 23:25:45 +01:00
bus37 = getTimeStates(ctx, hassClient, "sensor.tcl_37_1", "sensor.tcl_37_2", "sensor.tcl_37_3")
busC17 = getTimeStates(ctx, hassClient, "sensor.tcl_c17_1", "sensor.tcl_c17_2", "sensor.tcl_c17_3")
tramT1 = getTimeStates(ctx, hassClient, "sensor.tcl_t1_1", "sensor.tcl_t1_2", "sensor.tcl_t1_3")
2024-09-16 14:20:59 +02:00
}()
go func() {
defer wg.Done()
2024-10-12 15:47:32 +02:00
ctx, cancel := context.WithTimeout(ctx, 20*time.Second)
2024-09-16 14:20:59 +02:00
defer cancel()
var err error
velovRocBikes, err = hassClient.GetState(ctx, "sensor.velov_rocard_octavie_bikes")
if err != nil {
slog.ErrorContext(ctx, "error getting velov rocard bikes", "err", err)
}
velovRocStands, err = hassClient.GetState(ctx, "sensor.velov_rocard_octavie_stands")
2024-09-16 14:20:59 +02:00
if err != nil {
slog.ErrorContext(ctx, "error getting velov rocard stands", "err", err)
2024-09-16 14:20:59 +02:00
}
}()
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var err error
feteName, err = hassClient.GetState(ctx, "sensor.fete_du_jour")
2024-09-16 14:20:59 +02:00
if err != nil {
slog.ErrorContext(ctx, "error getting fete_du_jour", "err", err)
2024-09-16 14:20:59 +02:00
}
}()
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var err error
wthr, err = weatherClient.GetWeather(ctx)
if err != nil {
slog.ErrorContext(ctx, "error getting weather", "err", err)
2024-09-16 14:20:59 +02:00
}
}()
2024-09-18 22:26:40 +02:00
go func() {
defer wg.Done()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var err error
2024-09-16 14:20:59 +02:00
2024-09-18 22:26:40 +02:00
msg, err = hassClient.GetState(ctx, "input_text.e_paper_message")
if err != nil {
slog.ErrorContext(ctx, "error getting hass message", "err", err)
2024-09-18 22:26:40 +02:00
}
if msg != "" {
return
}
msg, err = hassClient.GetState(ctx, "input_text.proverbe_du_jour")
if err != nil {
slog.ErrorContext(ctx, "error getting hass proverbe", "err", err)
}
2024-09-18 22:26:40 +02:00
}()
2024-09-15 12:00:36 +02:00
2024-09-15 09:01:13 +02:00
img := newWhite()
gc := draw2dimg.NewGraphicContext(img)
2024-09-15 10:46:07 +02:00
gc.SetFillRule(draw2d.FillRuleWinding)
2024-09-15 13:07:32 +02:00
gc.SetFillColor(color.RGBA{255, 255, 255, 255})
2024-09-15 09:01:13 +02:00
gc.SetStrokeColor(color.RGBA{0, 0, 0, 255})
2024-09-16 14:20:59 +02:00
wg.Wait()
2024-12-16 23:25:45 +01:00
drawTCL(gc, "37", bus37, nowFunc(), rightX, 45)
drawTCL(gc, "C17", busC17, nowFunc(), rightX+120, 45)
drawTCL(gc, "T1", tramT1, nowFunc(), rightX+120, 205)
drawVelov(gc, "Rocard Octavie", velovRocBikes, velovRocStands, 365)
2024-09-15 23:25:17 +02:00
drawDate(gc, nowFunc())
drawFete(gc, feteName)
drawWeather(ctx, gc, wthr)
2024-09-18 22:26:40 +02:00
drawMsg(gc, msg)
2024-09-15 12:00:36 +02:00
return img, nil
}
2024-12-16 23:25:45 +01:00
func getTimeStates(ctx context.Context, hassClient *home_assistant.Client, entityIDs ...string) []time.Time {
var times []time.Time
for _, entityID := range entityIDs {
t, err := hassClient.GetTimeState(ctx, entityID)
if err != nil {
slog.ErrorContext(ctx, "error getting time state", "err", err, "entityID", entityID)
}
times = append(times, t)
}
return times
}
2024-09-18 22:26:40 +02:00
func drawMsg(gc *draw2dimg.GraphicContext, quote string) {
2024-11-26 18:26:45 +01:00
text(gc, quote, 15, leftX, 450, fonts.Italic)
2024-09-15 23:55:31 +02:00
}
func drawWeather(ctx context.Context, gc *draw2dimg.GraphicContext, wthr *weather.Prevision) {
2024-09-15 23:25:17 +02:00
if wthr == nil {
return
}
dailyLen := len(wthr.Daily)
dailyWeatherLen := len(wthr.Daily[0].Weather)
2024-11-26 19:10:11 +01:00
currentWeatherLen := len(wthr.Current.Weather)
if dailyLen == 0 || dailyWeatherLen == 0 || currentWeatherLen == 0 {
slog.ErrorContext(ctx, "missing daily or daily weather or current weather", "daily_len", dailyLen, "daily_weather_len", dailyWeatherLen, "current_weather_len", currentWeatherLen)
2024-09-15 23:25:17 +02:00
return
}
2024-11-26 19:10:11 +01:00
current := wthr.Current
currentWeather := current.Weather[0]
err := drawWeatherIcon(gc, currentWeather)
2024-09-15 23:25:17 +02:00
if err != nil {
slog.ErrorContext(ctx, "Failed to draw weather icon", "err", err)
2024-09-15 23:25:17 +02:00
}
2024-11-26 19:10:11 +01:00
text(gc, formatTemp(current.Temp), 23, leftX, 125, fonts.Regular)
text(gc, fmt.Sprintf("(%v)", formatTemp(current.FeelsLike)), 15, leftX+5, 150, fonts.Regular)
daily := wthr.Daily[0]
dailyWeather := daily.Weather[0]
2024-09-16 00:35:45 +02:00
2024-11-26 19:10:11 +01:00
const xAlign = 140
const fontSize = 18
2024-11-26 18:26:45 +01:00
text(gc, "journée", fontSize, xAlign, 35, fonts.Bold)
2024-11-26 19:10:11 +01:00
text(gc, dailyWeather.Description, fontSize, xAlign, 65, fonts.Regular)
text(gc, "max "+formatTemp(daily.Temp.Max), fontSize, xAlign, 95, fonts.Regular)
text(gc, fmt.Sprintf("pluie %v%%", formatPct(daily.Pop)), fontSize, xAlign, 125, fonts.Regular)
2024-11-26 19:39:34 +01:00
nextRainStart, nextRainEnd, probas := findNextRain(wthr.Hourly)
avg, maxProba := averageAndMax(probas)
if len(probas) > 0 {
text(gc, "\uE1B4", 14, xAlign, 155+fonts.IconYOffset, fonts.Icons)
text(gc, fmt.Sprintf("%v-%v %v%% %v%%", nextRainStart.Format("15h"), nextRainEnd.Format("15h"), formatPct(avg), formatPct(maxProba)), 14, xAlign+20, 155, fonts.Regular)
text(gc, "\uEDAA", 14, xAlign+95, 155+fonts.IconYOffset, fonts.Icons)
text(gc, "\uE4AE", 14, xAlign+155, 155+fonts.IconYOffset, fonts.Icons)
2024-11-26 19:10:11 +01:00
}
}
func formatPct(pct float64) int {
return int(math.Round(pct * 100))
}
2024-11-26 19:39:34 +01:00
func findNextRain(hourly []weather.Hourly) (time.Time, time.Time, []float64) {
2024-11-26 19:10:11 +01:00
if len(hourly) > 12 {
hourly = hourly[:12]
}
2024-11-26 19:39:34 +01:00
var (
start, end time.Time
probas []float64
)
2024-11-26 19:10:11 +01:00
for _, h := range hourly {
2024-11-26 19:39:34 +01:00
if h.Pop == 0 && start != (time.Time{}) {
end = hourlyToTime(h)
break
}
2024-11-26 19:10:11 +01:00
if h.Pop > 0 {
2024-11-26 19:39:34 +01:00
if start == (time.Time{}) {
start = hourlyToTime(h)
}
probas = append(probas, h.Pop)
2024-11-26 19:10:11 +01:00
}
}
2024-11-26 19:39:34 +01:00
return start, end, probas
}
func averageAndMax(probas []float64) (avg float64, max float64) {
if len(probas) == 0 {
return 0, 0
}
var sum float64
for _, proba := range probas {
sum += proba
if proba > max {
max = proba
}
}
return sum / float64(len(probas)), max
}
func hourlyToTime(h weather.Hourly) time.Time {
return time.Unix(int64(h.Dt), 0)
2024-09-15 23:25:17 +02:00
}
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
}
2024-09-16 00:35:45 +02:00
func formatTemp(temp float64) string {
return fmt.Sprintf("%v°C", int(math.Round(temp)))
2024-09-15 23:25:17 +02:00
}
func drawVelov(gc *draw2dimg.GraphicContext, title, bikes, stands string, yOffset float64) {
text(gc, title, 23, rightX, yOffset, fonts.Bold)
2024-11-26 02:34:53 +01:00
yOffset += 30
2024-11-26 18:26:45 +01:00
text(gc, "\uE0D6", 22, rightX, yOffset+fonts.IconYOffset, fonts.Icons) // bike icon
text(gc, bikes, 22, rightX+fonts.IconXOffset, yOffset, fonts.Regular)
2024-11-26 02:34:53 +01:00
nextCol := rightX + 100.0
2024-11-26 18:26:45 +01:00
text(gc, "\uEC08", 22, nextCol, yOffset+fonts.IconYOffset, fonts.Icons) // parking icon
text(gc, stands, 22, nextCol+fonts.IconXOffset, yOffset, fonts.Regular)
2024-09-15 23:25:17 +02:00
}
func drawDate(gc *draw2dimg.GraphicContext, now time.Time) {
2024-11-26 18:27:09 +01:00
text(gc, now.Format("15:04"), 110, leftX, 300, fonts.SemiBold)
2024-11-26 18:26:45 +01:00
text(gc, getDate(now), 30, leftX, 345, fonts.Regular)
2024-09-15 14:19:01 +02:00
}
func drawFete(gc *draw2dimg.GraphicContext, feteName string) {
if feteName == "" {
2024-09-15 23:25:17 +02:00
return
}
text(gc, fmt.Sprintf("On fête les %s", feteName), 18, leftX, 380, fonts.Regular)
2024-09-15 12:00:36 +02:00
}
func drawTCL(gc *draw2dimg.GraphicContext, title string, times []time.Time, now time.Time, x, yoffset float64) {
text(gc, "\uE106", 23, x, yoffset+fonts.IconYOffset, fonts.Icons)
text(gc, title, 23, x+fonts.IconXOffset, yoffset, fonts.Bold)
for j, t := range times {
if t == (time.Time{}) {
continue
2024-09-15 10:46:07 +02:00
}
delay := t.Sub(now).Truncate(time.Minute)
2024-12-16 19:45:16 +01:00
delayStr := "passé"
if delay > time.Minute {
delayStr = fmt.Sprintf("%v min", delay.Minutes())
} else if delay > 0 {
delayStr = "proche"
}
y := yoffset + float64(j+1)*35
2024-12-16 19:45:16 +01:00
text(gc, delayStr, 22, x, y, fonts.Regular)
2024-09-15 10:46:07 +02:00
}
2024-09-15 09:01:13 +02:00
}
2024-11-26 01:18:38 +01:00
func text(gc *draw2dimg.GraphicContext, s string, size, x, y float64, fontName string) {
2024-09-15 13:07:32 +02:00
gc.SetFillColor(color.RGBA{0, 0, 0, 255})
2024-09-15 09:01:13 +02:00
gc.SetFontData(draw2d.FontData{Name: fontName})
gc.SetFontSize(size)
2024-09-15 10:46:07 +02:00
gc.FillStringAt(s, x, y)
2024-09-15 09:01:13 +02:00
}
func newWhite() *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, epd.Width, epd.Height))
for y := 0; y < epd.Height; y++ {
for x := 0; x < epd.Width; x++ {
img.Set(x, y, color.White)
}
}
return img
}
func rect(gc *draw2dimg.GraphicContext, x1, y1, x2, y2 float64) {
gc.BeginPath()
gc.MoveTo(x1, y1)
gc.LineTo(x2, y1)
gc.LineTo(x2, y2)
gc.LineTo(x1, y2)
gc.Close()
gc.FillStroke()
}
2024-09-15 12:00:36 +02:00
func getDate(now time.Time) string {
2024-09-15 23:25:17 +02:00
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 "?"
2024-09-15 12:00:36 +02:00
}
func getDay(now time.Time) string {
if now.Day() == 1 {
return "1er"
}
return strconv.Itoa(now.Day())
}
func getMonth(t time.Time) string {
switch t.Month() {
case time.January:
return "jan."
case time.February:
return "fev."
case time.March:
return "mars"
case time.April:
return "avr."
case time.May:
return "mai"
case time.June:
return "juin"
case time.July:
return "juil."
case time.August:
return "août"
case time.September:
return "sept."
case time.October:
return "oct."
case time.November:
return "nov."
case time.December:
return "dec."
}
return "?"
}