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"
2024-10-21 23:14:55 +02:00
"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
)
2024-12-16 19:40:10 +01:00
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 19:40:10 +01:00
bus37_1 time . Time
bus37_2 time . Time
bus37_3 time . Time
busC17_1 time . Time
busC17_2 time . Time
busC17_3 time . Time
tramT1_1 time . Time
tramT1_2 time . Time
tramT1_3 time . Time
velovRocBikes string
velovRocStands string
feteName string
wthr * weather . Prevision
msg string
2024-09-16 14:20:59 +02:00
)
wg := & sync . WaitGroup { }
2024-12-16 19:40:10 +01:00
wg . Add ( 5 )
2024-09-16 14:20:59 +02:00
go func ( ) {
defer wg . Done ( )
2024-12-16 19:40:10 +01:00
ctx , cancel := context . WithTimeout ( ctx , 30 * time . Second )
2024-09-16 14:20:59 +02:00
defer cancel ( )
var err error
2024-12-16 19:40:10 +01:00
bus37_1 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_37_1" )
2024-09-16 14:20:59 +02:00
if err != nil {
2024-12-16 19:40:10 +01:00
slog . ErrorContext ( ctx , "error getting 37_1" , "err" , err )
}
bus37_2 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_37_2" )
if err != nil {
slog . ErrorContext ( ctx , "error getting 37_2" , "err" , err )
}
bus37_3 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_37_3" )
if err != nil {
slog . ErrorContext ( ctx , "error getting 37_3" , "err" , err )
2024-09-16 14:20:59 +02:00
}
2024-12-16 19:40:10 +01:00
busC17_1 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_c17_1" )
if err != nil {
slog . ErrorContext ( ctx , "error getting C17_1" , "err" , err )
}
busC17_2 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_c17_2" )
if err != nil {
slog . ErrorContext ( ctx , "error getting C17_2" , "err" , err )
}
busC17_3 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_c17_3" )
if err != nil {
slog . ErrorContext ( ctx , "error getting C17_3" , "err" , err )
}
2024-09-16 14:20:59 +02:00
2024-12-16 19:40:10 +01:00
tramT1_1 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_t1_1" )
if err != nil {
slog . ErrorContext ( ctx , "error getting T1_1" , "err" , err )
}
tramT1_2 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_t1_2" )
2024-09-16 14:20:59 +02:00
if err != nil {
2024-12-16 19:40:10 +01:00
slog . ErrorContext ( ctx , "error getting T1_2" , "err" , err )
2024-09-16 14:20:59 +02:00
}
2024-12-16 19:40:10 +01:00
tramT1_3 , err = hassClient . GetTimeState ( ctx , "sensor.tcl_t1_3" )
if err != nil {
slog . ErrorContext ( ctx , "error getting T1_3" , "err" , err )
}
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
2024-12-16 19:40:10 +01:00
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 {
2024-12-16 19:40:10 +01:00
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
2024-12-16 19:40:10 +01:00
feteName , err = hassClient . GetState ( ctx , "sensor.fete_du_jour" )
2024-09-16 14:20:59 +02:00
if err != nil {
2024-12-16 19:40:10 +01:00
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 {
2024-10-21 23:14:55 +02:00
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 {
2024-10-21 23:14:55 +02:00
slog . ErrorContext ( ctx , "error getting hass message" , "err" , err )
2024-09-18 22:26:40 +02:00
}
2024-12-16 19:40:10 +01: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 19:40:10 +01:00
drawTCL ( gc , "37" , [ ] time . Time { bus37_1 , bus37_2 , bus37_3 } , nowFunc ( ) , rightX , 45 )
drawTCL ( gc , "C17" , [ ] time . Time { busC17_1 , busC17_2 , busC17_3 } , nowFunc ( ) , rightX + 120 , 45 )
drawTCL ( gc , "T1" , [ ] time . Time { tramT1_1 , tramT1_2 , tramT1_3 } , nowFunc ( ) , rightX + 120 , 205 )
drawVelov ( gc , "Rocard Octavie" , velovRocBikes , velovRocStands , 365 )
2024-09-15 23:25:17 +02:00
drawDate ( gc , nowFunc ( ) )
2024-12-16 19:40:10 +01:00
drawFete ( gc , feteName )
2024-10-21 23:14:55 +02:00
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-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
}
2024-10-21 23:14:55 +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
}
2024-10-21 23:14:55 +02:00
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 {
2024-10-21 23:14:55 +02:00
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
2024-11-26 02:15:59 +01:00
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
}
2024-12-16 19:40:10 +01: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
2024-12-16 19:40:10 +01:00
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
2024-12-16 19:40:10 +01:00
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
}
2024-12-16 19:40:10 +01:00
func drawFete ( gc * draw2dimg . GraphicContext , feteName string ) {
if feteName == "" {
2024-09-15 23:25:17 +02:00
return
}
2024-12-16 19:40:10 +01:00
text ( gc , fmt . Sprintf ( "On fête les %s" , feteName ) , 18 , leftX , 380 , fonts . Regular )
2024-09-15 12:00:36 +02:00
}
2024-12-16 19:40:10 +01: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
}
2024-12-16 19:40:10 +01:00
delay := t . Sub ( now ) . Truncate ( time . Minute )
y := yoffset + float64 ( j + 1 ) * 35
text ( gc , fmt . Sprintf ( "%v min" , delay . Minutes ( ) ) , 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 "?"
}