From 7996aecc07386c017cef93f55a530d0c17cdf2e9 Mon Sep 17 00:00:00 2001 From: Pedro Pereira Date: Sun, 8 Sep 2024 15:00:15 +0100 Subject: [PATCH 1/3] First openmeteo implementation --- backends/open-meteo.com.go | 217 +++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 backends/open-meteo.com.go diff --git a/backends/open-meteo.com.go b/backends/open-meteo.com.go new file mode 100644 index 0000000..d836416 --- /dev/null +++ b/backends/open-meteo.com.go @@ -0,0 +1,217 @@ +package backends + +import ( + "encoding/json" + "flag" + "fmt" + "github.com/schachmat/wego/iface" + "io" + "log" + "net/http" + "regexp" + "strings" + "time" +) + +type openmeteoConfig struct { + apiKey string + language string + debug bool +} + +type curCond struct { + Time int64 `json:"time"` + Interval int `json:"interval"` + Temperature2M *float32 `json:"temperature_2m"` + ApparentTemperature *float32 `json:"apparent_temperature"` + IsDay int `json:"is_day"` + WeatherCode int `json:"weather_code"` +} + +type Daily struct { + Time []int64 `json:"time"` + WeatherCode []int `json:"weather_code"` + Temperature2MMax []*float32 `json:"temperature_2m_max"` + ApparentTemperatureMax []*float32 `json:"apparent_temperature_max"` + Sunrise []int64 `json:"sunrise"` + Sunset []int64 `json:"sunset"` +} +type HourlyUnits struct { + Time string `json:"time"` + Temperature2M string `json:"temperature_2m"` + ApparentTemperature string `json:"apparent_temperature"` + WeatherCode string `json:"weather_code"` +} +type Hourly struct { + Time []int64 `json:"time"` + Temperature2M []*float32 `json:"temperature_2m"` + ApparentTemperature []*float32 `json:"apparent_temperature"` + WeatherCode []int `json:"weather_code"` +} + +type openmeteoResponse struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + GenerationtimeMs float64 `json:"generationtime_ms"` + UtcOffsetSeconds int `json:"utc_offset_seconds"` + Timezone string `json:"timezone"` + TimezoneAbbreviation string `json:"timezone_abbreviation"` + Elevation float64 `json:"elevation"` + CurrentUnits struct { + Time string `json:"time"` + Interval string `json:"interval"` + Temperature2M string `json:"temperature_2m"` + ApparentTemperature string `json:"apparent_temperature"` + IsDay string `json:"is_day"` + WeatherCode string `json:"weather_code"` + } `json:"current_units"` + Current curCond `json:"current"` + HourlyUnits HourlyUnits `json:"hourly_units"` + Hourly Hourly `json:"hourly"` + DailyUnits struct { + Time string `json:"time"` + WeatherCode string `json:"weather_code"` + Temperature2MMax string `json:"temperature_2m_max"` + ApparentTemperatureMax string `json:"apparent_temperature_max"` + Sunrise string `json:"sunrise"` + Sunset string `json:"sunset"` + } `json:"daily_units"` + Daily Daily +} + +const ( + openmeteoURI = "https://api.open-meteo.com/v1/forecast?" +) + +var ( + codemap = map[int]iface.WeatherCode{ + 0: iface.CodeSunny, + 1: iface.CodePartlyCloudy, + 2: iface.CodePartlyCloudy, + 3: iface.CodePartlyCloudy, + 45: iface.CodeFog, + 48: iface.CodeFog, + 51: iface.CodeLightRain, + 53: iface.CodeLightRain, + 55: iface.CodeLightRain, + 56: iface.CodeLightSleet, + 57: iface.CodeLightSleet, + 61: iface.CodeLightShowers, + 63: iface.CodeLightShowers, + 65: iface.CodeLightShowers, + 66: iface.CodeHeavyRain, + 67: iface.CodeHeavyRain, + } +) + +func (opmeteo *openmeteoConfig) Setup() { + flag.StringVar(&opmeteo.apiKey, "openmeteo-api-key", "", "openmeteo backend: the api `KEY` to use if commercial usage") + flag.BoolVar(&opmeteo.debug, "openmeteo-debug", false, "openmeteo backend: print raw requests and responses") +} + +func (opmeteo *openmeteoConfig) parseDaily(dailyInfo Hourly) []iface.Day { + var forecast []iface.Day + var day *iface.Day + + for ind, dayTime := range dailyInfo.Time { + + //day := new(iface.Day) + cond := new(iface.Cond) + + cond.Code = codemap[dailyInfo.WeatherCode[ind]] + cond.TempC = dailyInfo.Temperature2M[ind] + cond.FeelsLikeC = dailyInfo.ApparentTemperature[ind] + cond.Time = time.Unix(dayTime, 0) + + if day == nil { + day = new(iface.Day) + day.Date = cond.Time + } + if day.Date.Day() == cond.Time.Day() { + day.Slots = append(day.Slots, *cond) + } + if day.Date.Day() != cond.Time.Day() { + forecast = append(forecast, *day) + + day = new(iface.Day) + day.Date = cond.Time + day.Slots = append(day.Slots, *cond) + } + } + + return forecast +} + +func parseCurCond(current curCond) (ret iface.Cond) { + + ret.Time = time.Unix(current.Time, 0) + + ret.Code = iface.CodeUnknown + if val, ok := codemap[current.WeatherCode]; ok { + ret.Code = val + } + + ret.TempC = current.Temperature2M + ret.FeelsLikeC = current.ApparentTemperature + return ret + +} + +func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { + var ret iface.Data + var params []string + var loc string + + if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); matched && err == nil { + s := strings.Split(location, ",") + loc = fmt.Sprintf("latitude=%s&longitude=%s", s[0], s[1]) + } + if len(location) > 0 { + params = append(params, loc) + } + params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code&hourly=temperature_2m,apparent_temperature,weather_code&daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset&timeformat=unixtime&forecast_days=3") + + requri := openmeteoURI + strings.Join(params, "&") + + res, err := http.Get(requri) + if err != nil { + log.Fatal("Unable to get weather data: ", err) + } else if res.StatusCode != 200 { + log.Fatal("Unable to get weather data: http status ", res.StatusCode) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + if opmeteo.debug { + log.Println("Weather request:", requri) + log.Println("Weather response:", string(body)) + } + + var resp openmeteoResponse + if err = json.Unmarshal(body, &resp); err != nil { + log.Println(err) + } + + ret.Current = parseCurCond(resp.Current) + ret.Location = location + + forecast := opmeteo.parseDaily(resp.Hourly) + + if len(forecast) > 0 { + forecast[0].Astronomy.Sunset = time.Unix(resp.Daily.Sunset[0], 0) + forecast[0].Astronomy.Sunrise = time.Unix(resp.Daily.Sunrise[0], 0) + ret.Forecast = forecast + } + + //jcart, _ := json.MarshalIndent(ret, "", "\t") + //fmt.Println(string(jcart)) + return ret +} + +func init() { + iface.AllBackends["openmeteo"] = &openmeteoConfig{} +} From 311d6983b71cacf5b20f2f64b638d79d117a6ee9 Mon Sep 17 00:00:00 2001 From: Pedro Pereira Date: Sun, 8 Sep 2024 15:33:52 +0100 Subject: [PATCH 2/3] Add wind direction and debug output --- backends/open-meteo.com.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backends/open-meteo.com.go b/backends/open-meteo.com.go index d836416..6852638 100644 --- a/backends/open-meteo.com.go +++ b/backends/open-meteo.com.go @@ -4,13 +4,14 @@ import ( "encoding/json" "flag" "fmt" - "github.com/schachmat/wego/iface" "io" "log" "net/http" "regexp" "strings" "time" + + "github.com/schachmat/wego/iface" ) type openmeteoConfig struct { @@ -26,6 +27,7 @@ type curCond struct { ApparentTemperature *float32 `json:"apparent_temperature"` IsDay int `json:"is_day"` WeatherCode int `json:"weather_code"` + WindDirection10M *int `json:"wind_direction_10m"` } type Daily struct { @@ -47,6 +49,7 @@ type Hourly struct { Temperature2M []*float32 `json:"temperature_2m"` ApparentTemperature []*float32 `json:"apparent_temperature"` WeatherCode []int `json:"weather_code"` + WindDirection10M []*int `json:"wind_direction_10m"` } type openmeteoResponse struct { @@ -115,13 +118,13 @@ func (opmeteo *openmeteoConfig) parseDaily(dailyInfo Hourly) []iface.Day { for ind, dayTime := range dailyInfo.Time { - //day := new(iface.Day) cond := new(iface.Cond) cond.Code = codemap[dailyInfo.WeatherCode[ind]] cond.TempC = dailyInfo.Temperature2M[ind] cond.FeelsLikeC = dailyInfo.ApparentTemperature[ind] cond.Time = time.Unix(dayTime, 0) + cond.WinddirDegree = dailyInfo.WindDirection10M[ind] if day == nil { day = new(iface.Day) @@ -153,6 +156,7 @@ func parseCurCond(current curCond) (ret iface.Cond) { ret.TempC = current.Temperature2M ret.FeelsLikeC = current.ApparentTemperature + ret.WinddirDegree = current.WindDirection10M return ret } @@ -169,7 +173,7 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { if len(location) > 0 { params = append(params, loc) } - params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code&hourly=temperature_2m,apparent_temperature,weather_code&daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset&timeformat=unixtime&forecast_days=3") + params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code&hourly=temperature_2m,apparent_temperature,weather_code,wind_direction_10m&daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset&timeformat=unixtime&forecast_days=3") requri := openmeteoURI + strings.Join(params, "&") @@ -188,7 +192,8 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { if opmeteo.debug { log.Println("Weather request:", requri) - log.Println("Weather response:", string(body)) + b, _ := json.MarshalIndent(body, "", "\t") + fmt.Println("Weather response:", string(b)) } var resp openmeteoResponse @@ -207,8 +212,6 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { ret.Forecast = forecast } - //jcart, _ := json.MarshalIndent(ret, "", "\t") - //fmt.Println(string(jcart)) return ret } From 2c128eccc0e718113a2bbbce676fe84b2df4aca2 Mon Sep 17 00:00:00 2001 From: Pedro Pereira Date: Mon, 9 Sep 2024 19:37:46 +0100 Subject: [PATCH 3/3] Use numer of days --- backends/open-meteo.com.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backends/open-meteo.com.go b/backends/open-meteo.com.go index 6852638..696ba1e 100644 --- a/backends/open-meteo.com.go +++ b/backends/open-meteo.com.go @@ -166,6 +166,10 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { var params []string var loc string + if numdays <= 0 { + log.Fatal("Number of days less than 1 ") + } + if matched, err := regexp.MatchString(`^-?[0-9]*(\.[0-9]+)?,-?[0-9]*(\.[0-9]+)?$`, location); matched && err == nil { s := strings.Split(location, ",") loc = fmt.Sprintf("latitude=%s&longitude=%s", s[0], s[1]) @@ -173,7 +177,10 @@ func (opmeteo *openmeteoConfig) Fetch(location string, numdays int) iface.Data { if len(location) > 0 { params = append(params, loc) } - params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code&hourly=temperature_2m,apparent_temperature,weather_code,wind_direction_10m&daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset&timeformat=unixtime&forecast_days=3") + params = append(params, "current=temperature_2m,apparent_temperature,is_day,weather_code,wind_direction_10m") + params = append(params, "hourly=temperature_2m,apparent_temperature,weather_code,wind_direction_10m") + params = append(params, "daily=weather_code,temperature_2m_max,apparent_temperature_max,sunrise,sunset") + params = append(params, fmt.Sprintf("timeformat=unixtime&forecast_days=%d", numdays)) requri := openmeteoURI + strings.Join(params, "&")