Skip to content

Commit fdd48b0

Browse files
committed
fix: open-meteo tweaks
1 parent cb950f1 commit fdd48b0

File tree

8 files changed

+1505
-418
lines changed

8 files changed

+1505
-418
lines changed

docs/datasources/openmeteo.md

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Open-Meteo Weather Datasource
22

3-
The Open-Meteo datasource fetches current weather data for any location worldwide using the free Open-Meteo API. It automatically geocodes location names and provides comprehensive weather information including temperature, wind, humidity, pressure, and UV index.
3+
The Open-Meteo datasource fetches current weather data and hourly forecasts for any location worldwide using the free Open-Meteo API. It automatically geocodes location names and provides comprehensive weather information including temperature, wind, humidity, pressure, UV index, and rain predictions.
44

55
## Features
66

77
- 🌍 **Worldwide Coverage** - Fetch weather for any location globally
88
- 📍 **Automatic Geocoding** - Simply provide a location name (city, town, etc.)
99
- 🌡️ **Comprehensive Data** - Temperature, feels-like, wind, humidity, pressure, UV index
10+
- 🌧️ **Rain Predictions** - Alerts when rain is expected today with precipitation probability
11+
- 📊 **Hourly Forecast** - 24-hour forecast with detailed hourly weather conditions
1012
- 🎨 **Weather Icons** - Visual representation with emoji icons for each weather condition
1113
- 🆓 **Free API** - No API key required, uses the free Open-Meteo service
1214
- 🔄 **Real-time Updates** - Configurable fetch intervals for up-to-date weather
@@ -79,6 +81,21 @@ The datasource stores the following fields for each weather report:
7981
| `surface_pressure` | REAL | Surface pressure (hPa) |
8082
| `sealevel_pressure` | REAL | Sea level pressure (hPa) |
8183
| `uv_index` | REAL | Maximum UV index for the day |
84+
| `hourly_forecast` | TEXT | JSON array of hourly forecast data for today (24 hours) |
85+
86+
### Hourly Forecast Data
87+
88+
Each hourly forecast entry contains:
89+
90+
| Field | Type | Description |
91+
|-------|------|-------------|
92+
| `time` | TEXT | ISO 8601 timestamp for the hour |
93+
| `temperature` | REAL | Hourly temperature (°C) |
94+
| `weather_code` | INTEGER | WMO weather code for the hour |
95+
| `weather_description` | TEXT | Human-readable condition |
96+
| `precipitation` | REAL | Precipitation amount (mm) |
97+
| `precipitation_probability` | INTEGER | Chance of precipitation (%) |
98+
| `humidity` | REAL | Relative humidity (%) |
8299

83100
## Weather Codes and Icons
84101

@@ -166,11 +183,24 @@ When fetching or streaming data, you'll see output like this:
166183
This datasource uses the free [Open-Meteo API](https://open-meteo.com/):
167184

168185
- **Geocoding**: `geocoding-api.open-meteo.com` - Converts location names to coordinates
169-
- **Weather Data**: `api.open-meteo.com` - Provides current weather and forecasts
186+
- **Weather Data**: `api.open-meteo.com` - Provides current weather and 24-hour forecasts
170187
- **Air Quality**: `air-quality-api.open-meteo.com` - UV index and air quality data
171188
- **No API Key Required**: Free for non-commercial use
172189
- **Attribution**: Data provided by Open-Meteo.com
173190

191+
## Rain Prediction & Forecast Display
192+
193+
The weather renderer includes:
194+
195+
- **Rain Alert Badge** - Prominent alert when rain is expected today, showing maximum precipitation probability
196+
- **Collapsible Hourly Forecast** - Interactive table with 24-hour forecast including:
197+
- Time and weather condition icons
198+
- Hourly temperature
199+
- Precipitation amount and probability
200+
- Humidity levels
201+
202+
The rain detection automatically checks all hourly forecasts for rain codes (51-82) and displays an alert if rain is expected at any point during the day.
203+
174204
## Location Examples
175205

176206
You can use various location formats:
@@ -200,8 +230,9 @@ location = 'København'
200230
- **API Rate Limits**: Open-Meteo is free but has reasonable rate limits
201231
- **Update Frequency**: Weather data typically updates every 15-60 minutes
202232
- **Recommended Interval**: 30 minutes to 1 hour is usually sufficient
203-
- **Storage**: Creates one block per fetch, deduplicates based on weather conditions
233+
- **Storage**: Creates one block per fetch with 24-hour forecast data, deduplicates based on weather conditions
204234
- **Network**: Requires 3 API calls per fetch (geocoding, weather, air quality)
235+
- **Forecast Data**: Stores up to 24 hours of hourly forecast data per block
205236

206237
## Troubleshooting
207238

pkg/datasources/openmeteo/blocks.go

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type WeatherBlock struct {
3232
surfacePressure float64
3333
sealevelPressure float64
3434
uvIndex float64
35+
hourlyForecast []map[string]interface{}
3536
}
3637

3738
func NewWeatherBlock(
@@ -43,6 +44,7 @@ func NewWeatherBlock(
4344
weatherCode int,
4445
weatherDescription string,
4546
humidity, apparentTemperature, surfacePressure, sealevelPressure, uvIndex float64,
47+
hourlyForecast []map[string]interface{},
4648
createdAt time.Time,
4749
source string,
4850
) *WeatherBlock {
@@ -68,6 +70,7 @@ func NewWeatherBlock(
6870
"sealevel_pressure": sealevelPressure,
6971
"uv_index": uvIndex,
7072
"source": source,
73+
"hourly_forecast": hourlyForecast,
7174
}
7275

7376
// Use content-based ID so we only create new blocks when weather changes significantly
@@ -96,6 +99,7 @@ func NewWeatherBlock(
9699
surfacePressure: surfacePressure,
97100
sealevelPressure: sealevelPressure,
98101
uvIndex: uvIndex,
102+
hourlyForecast: hourlyForecast,
99103
}
100104
}
101105

@@ -133,22 +137,23 @@ func (b *WeatherBlock) Summary() string {
133137
}
134138

135139
// Custom accessor methods
136-
func (b *WeatherBlock) Location() string { return b.location }
137-
func (b *WeatherBlock) Country() string { return b.country }
138-
func (b *WeatherBlock) Latitude() float64 { return b.latitude }
139-
func (b *WeatherBlock) Longitude() float64 { return b.longitude }
140-
func (b *WeatherBlock) Timezone() string { return b.timezone }
141-
func (b *WeatherBlock) Population() int64 { return b.population }
142-
func (b *WeatherBlock) Temperature() float64 { return b.temperature }
143-
func (b *WeatherBlock) WindSpeed() float64 { return b.windSpeed }
144-
func (b *WeatherBlock) WindDirection() float64 { return b.windDirection }
145-
func (b *WeatherBlock) WeatherCode() int { return b.weatherCode }
146-
func (b *WeatherBlock) WeatherDescription() string { return b.weatherDescription }
147-
func (b *WeatherBlock) Humidity() float64 { return b.humidity }
148-
func (b *WeatherBlock) ApparentTemperature() float64 { return b.apparentTemperature }
149-
func (b *WeatherBlock) SurfacePressure() float64 { return b.surfacePressure }
150-
func (b *WeatherBlock) SealevelPressure() float64 { return b.sealevelPressure }
151-
func (b *WeatherBlock) UVIndex() float64 { return b.uvIndex }
140+
func (b *WeatherBlock) Location() string { return b.location }
141+
func (b *WeatherBlock) Country() string { return b.country }
142+
func (b *WeatherBlock) Latitude() float64 { return b.latitude }
143+
func (b *WeatherBlock) Longitude() float64 { return b.longitude }
144+
func (b *WeatherBlock) Timezone() string { return b.timezone }
145+
func (b *WeatherBlock) Population() int64 { return b.population }
146+
func (b *WeatherBlock) Temperature() float64 { return b.temperature }
147+
func (b *WeatherBlock) WindSpeed() float64 { return b.windSpeed }
148+
func (b *WeatherBlock) WindDirection() float64 { return b.windDirection }
149+
func (b *WeatherBlock) WeatherCode() int { return b.weatherCode }
150+
func (b *WeatherBlock) WeatherDescription() string { return b.weatherDescription }
151+
func (b *WeatherBlock) Humidity() float64 { return b.humidity }
152+
func (b *WeatherBlock) ApparentTemperature() float64 { return b.apparentTemperature }
153+
func (b *WeatherBlock) SurfacePressure() float64 { return b.surfacePressure }
154+
func (b *WeatherBlock) SealevelPressure() float64 { return b.sealevelPressure }
155+
func (b *WeatherBlock) UVIndex() float64 { return b.uvIndex }
156+
func (b *WeatherBlock) HourlyForecast() []map[string]interface{} { return b.hourlyForecast }
152157

153158
func (b *WeatherBlock) Type() string { return "openmeteo" }
154159

@@ -173,6 +178,7 @@ func (b *WeatherBlock) Factory(genericBlock *core.GenericBlock, source string) c
173178
surfacePressure := getFloatFromMetadata(metadata, "surface_pressure", 0.0)
174179
sealevelPressure := getFloatFromMetadata(metadata, "sealevel_pressure", 0.0)
175180
uvIndex := getFloatFromMetadata(metadata, "uv_index", 0.0)
181+
hourlyForecast := getSliceFromMetadata(metadata, "hourly_forecast")
176182

177183
return &WeatherBlock{
178184
id: genericBlock.ID(),
@@ -196,6 +202,7 @@ func (b *WeatherBlock) Factory(genericBlock *core.GenericBlock, source string) c
196202
surfacePressure: surfacePressure,
197203
sealevelPressure: sealevelPressure,
198204
uvIndex: uvIndex,
205+
hourlyForecast: hourlyForecast,
199206
}
200207
}
201208

@@ -219,6 +226,7 @@ func (f *BlockFactory) CreateFromGeneric(id, text string, createdAt time.Time, s
219226
surfacePressure := getFloatFromMetadata(metadata, "surface_pressure", 0.0)
220227
sealevelPressure := getFloatFromMetadata(metadata, "sealevel_pressure", 0.0)
221228
uvIndex := getFloatFromMetadata(metadata, "uv_index", 0.0)
229+
hourlyForecast := getSliceFromMetadata(metadata, "hourly_forecast")
222230

223231
return &WeatherBlock{
224232
id: id,
@@ -242,6 +250,7 @@ func (f *BlockFactory) CreateFromGeneric(id, text string, createdAt time.Time, s
242250
surfacePressure: surfacePressure,
243251
sealevelPressure: sealevelPressure,
244252
uvIndex: uvIndex,
253+
hourlyForecast: hourlyForecast,
245254
}
246255
}
247256

@@ -255,6 +264,24 @@ func getStringFromMetadata(metadata map[string]interface{}, key, defaultValue st
255264
return defaultValue
256265
}
257266

267+
func getSliceFromMetadata(metadata map[string]interface{}, key string) []map[string]interface{} {
268+
if value, exists := metadata[key]; exists {
269+
if slice, ok := value.([]interface{}); ok {
270+
result := make([]map[string]interface{}, 0, len(slice))
271+
for _, item := range slice {
272+
if m, ok := item.(map[string]interface{}); ok {
273+
result = append(result, m)
274+
}
275+
}
276+
return result
277+
}
278+
if slice, ok := value.([]map[string]interface{}); ok {
279+
return slice
280+
}
281+
}
282+
return []map[string]interface{}{}
283+
}
284+
258285
func getFloatFromMetadata(metadata map[string]interface{}, key string, defaultValue float64) float64 {
259286
if value, exists := metadata[key]; exists {
260287
switch v := value.(type) {

pkg/datasources/openmeteo/datasource.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ type WeatherResult struct {
6464
ApparentTemperature []float64 `json:"apparent_temperature"`
6565
SurfacePressure []float64 `json:"surface_pressure"`
6666
PressureMSL []float64 `json:"pressure_msl"`
67+
Temperature []float64 `json:"temperature_2m"`
68+
WeatherCode []int `json:"weather_code"`
69+
Precipitation []float64 `json:"precipitation"`
70+
PrecipitationProb []int `json:"precipitation_probability"`
6771
} `json:"hourly"`
6872
}
6973

@@ -125,6 +129,7 @@ func (d *Datasource) Schema() map[string]any {
125129
"surface_pressure": "REAL",
126130
"sealevel_pressure": "REAL",
127131
"uv_index": "REAL",
132+
"hourly_forecast": "TEXT",
128133
}
129134
}
130135

@@ -231,7 +236,8 @@ func (d *Datasource) fetchWeather(ctx context.Context, lat, lon float64) (*Weath
231236
params.Add("latitude", fmt.Sprintf("%.4f", lat))
232237
params.Add("longitude", fmt.Sprintf("%.4f", lon))
233238
params.Add("current", "temperature_2m,wind_speed_10m,wind_direction_10m,weather_code")
234-
params.Add("hourly", "relative_humidity_2m,apparent_temperature,surface_pressure,pressure_msl")
239+
params.Add("hourly", "relative_humidity_2m,apparent_temperature,surface_pressure,pressure_msl,temperature_2m,weather_code,precipitation,precipitation_probability")
240+
params.Add("forecast_days", "1")
235241
params.Add("timezone", "auto")
236242

237243
reqURL := fmt.Sprintf("%s?%s", forecastURL, params.Encode())
@@ -338,6 +344,32 @@ func (d *Datasource) createWeatherBlock(
338344
}
339345
}
340346

347+
// Prepare hourly forecast data
348+
hourlyForecast := make([]map[string]interface{}, 0)
349+
maxHours := 24 // Only store today's forecast
350+
for i := 0; i < maxHours && i < len(weather.Hourly.Time); i++ {
351+
hourData := map[string]interface{}{
352+
"time": weather.Hourly.Time[i],
353+
}
354+
if i < len(weather.Hourly.Temperature) {
355+
hourData["temperature"] = weather.Hourly.Temperature[i]
356+
}
357+
if i < len(weather.Hourly.WeatherCode) {
358+
hourData["weather_code"] = weather.Hourly.WeatherCode[i]
359+
hourData["weather_description"] = translateWeatherCode(weather.Hourly.WeatherCode[i])
360+
}
361+
if i < len(weather.Hourly.Precipitation) {
362+
hourData["precipitation"] = weather.Hourly.Precipitation[i]
363+
}
364+
if i < len(weather.Hourly.PrecipitationProb) {
365+
hourData["precipitation_probability"] = weather.Hourly.PrecipitationProb[i]
366+
}
367+
if i < len(weather.Hourly.RelativeHumidity) {
368+
hourData["humidity"] = weather.Hourly.RelativeHumidity[i]
369+
}
370+
hourlyForecast = append(hourlyForecast, hourData)
371+
}
372+
341373
weatherDesc := translateWeatherCode(weather.Current.WeatherCode)
342374

343375
sourceName := d.instanceName
@@ -362,6 +394,7 @@ func (d *Datasource) createWeatherBlock(
362394
surfacePressure,
363395
sealevelPressure,
364396
maxUVIndex,
397+
hourlyForecast,
365398
createdAt,
366399
sourceName,
367400
)

pkg/datasources/openmeteo/renderer/renderer.go

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,20 @@
11
package renderer
22

33
import (
4+
"bytes"
5+
"context"
46
_ "embed"
57
"html/template"
6-
"strings"
78

89
"github.com/rubiojr/ergs/pkg/core"
910
"github.com/rubiojr/ergs/pkg/render"
1011
)
1112

12-
//go:embed template.html
13-
var weatherTemplate string
13+
//go:embed weather.css
14+
var weatherCSS string
1415

15-
// WeatherRenderer renders weather data blocks
16-
type WeatherRenderer struct {
17-
template *template.Template
18-
}
16+
// WeatherRenderer renders weather data blocks using templ
17+
type WeatherRenderer struct{}
1918

2019
// init function automatically registers this renderer with the global registry
2120
func init() {
@@ -27,28 +26,23 @@ func init() {
2726

2827
// NewWeatherRenderer creates a new weather renderer
2928
func NewWeatherRenderer() *WeatherRenderer {
30-
tmpl, err := template.New("openmeteo").Funcs(render.GetTemplateFuncs()).Parse(weatherTemplate)
31-
if err != nil {
32-
return nil
33-
}
34-
35-
return &WeatherRenderer{
36-
template: tmpl,
37-
}
29+
return &WeatherRenderer{}
3830
}
3931

40-
// Render creates an HTML representation of a weather block
32+
// Render creates an HTML representation of a weather block using templ
4133
func (r *WeatherRenderer) Render(block core.Block) template.HTML {
42-
data := render.TemplateData{
43-
Block: block,
44-
Metadata: block.Metadata(),
45-
Links: render.ExtractLinks(block.Text()),
46-
}
34+
var buf bytes.Buffer
35+
36+
// Inject CSS styles (only once per page load, ideally)
37+
buf.WriteString("<style>")
38+
buf.WriteString(weatherCSS)
39+
buf.WriteString("</style>")
4740

48-
var buf strings.Builder
49-
err := r.template.Execute(&buf, data)
41+
// Use the templ component to render
42+
component := WeatherBlock(block)
43+
err := component.Render(context.Background(), &buf)
5044
if err != nil {
51-
return template.HTML("Error rendering weather template")
45+
return template.HTML("Error rendering weather template: " + err.Error())
5246
}
5347

5448
return template.HTML(buf.String())

0 commit comments

Comments
 (0)