Skip to content

Commit 2d0feda

Browse files
fix(weather): restore OpenWeatherMap v2.5 support and unify hourly handling
- restore handling for v2.5 endpoints (/weather and /forecast) - keep the current hour visible in hourly mode while dropping older entries
1 parent 2e97e29 commit 2d0feda

File tree

5 files changed

+695
-23
lines changed

5 files changed

+695
-23
lines changed

defaultmodules/weather/providers/openweathermap.js

Lines changed: 151 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,28 +96,41 @@ class OpenWeatherMapProvider {
9696

9797
#handleResponse (data) {
9898
try {
99-
// Set location name from timezone
100-
if (data.timezone) {
101-
this.locationName = data.timezone;
102-
}
103-
10499
let weatherData;
105-
const onecallData = this.#generateWeatherObjectsFromOnecall(data);
106-
107-
switch (this.config.type) {
108-
case "current":
109-
weatherData = onecallData.current;
110-
break;
111-
case "forecast":
112-
case "daily":
113-
weatherData = onecallData.days;
114-
break;
115-
case "hourly":
116-
weatherData = onecallData.hours;
117-
break;
118-
default:
119-
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
120-
throw new Error(`Unknown weather type: ${this.config.type}`);
100+
101+
if (this.config.weatherEndpoint === "/onecall") {
102+
// One Call API (v3.0)
103+
if (data.timezone) {
104+
this.locationName = data.timezone;
105+
}
106+
107+
const onecallData = this.#generateWeatherObjectsFromOnecall(data);
108+
109+
switch (this.config.type) {
110+
case "current":
111+
weatherData = onecallData.current;
112+
break;
113+
case "forecast":
114+
case "daily":
115+
weatherData = onecallData.days;
116+
break;
117+
case "hourly":
118+
weatherData = onecallData.hours;
119+
break;
120+
default:
121+
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
122+
throw new Error(`Unknown weather type: ${this.config.type}`);
123+
}
124+
} else if (this.config.weatherEndpoint === "/weather") {
125+
// Current weather endpoint (API v2.5)
126+
weatherData = this.#generateWeatherObjectFromCurrentWeather(data);
127+
} else if (this.config.weatherEndpoint === "/forecast") {
128+
// 3-hourly forecast endpoint (API v2.5)
129+
weatherData = this.config.type === "hourly"
130+
? this.#generateHourlyWeatherObjectsFromForecast(data)
131+
: this.#generateDailyWeatherObjectsFromForecast(data);
132+
} else {
133+
throw new Error(`Unknown weather endpoint: ${this.config.weatherEndpoint}`);
121134
}
122135

123136
if (weatherData && this.onDataCallback) {
@@ -134,6 +147,123 @@ class OpenWeatherMapProvider {
134147
}
135148
}
136149

150+
#generateWeatherObjectFromCurrentWeather (data) {
151+
const timezoneOffsetMinutes = (data.timezone ?? 0) / 60;
152+
153+
if (data.name && data.sys?.country) {
154+
this.locationName = `${data.name}, ${data.sys.country}`;
155+
} else if (data.name) {
156+
this.locationName = data.name;
157+
}
158+
159+
const weather = {};
160+
weather.date = weatherUtils.applyTimezoneOffset(new Date(data.dt * 1000), timezoneOffsetMinutes);
161+
weather.temperature = data.main.temp;
162+
weather.feelsLikeTemp = data.main.feels_like;
163+
weather.humidity = data.main.humidity;
164+
weather.windSpeed = data.wind.speed;
165+
weather.windFromDirection = data.wind.deg;
166+
weather.weatherType = weatherUtils.convertWeatherType(data.weather[0].icon);
167+
weather.sunrise = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunrise * 1000), timezoneOffsetMinutes);
168+
weather.sunset = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunset * 1000), timezoneOffsetMinutes);
169+
170+
return weather;
171+
}
172+
173+
#extractThreeHourPrecipitation (forecast) {
174+
const rain = Number.parseFloat(forecast.rain?.["3h"] ?? "") || 0;
175+
const snow = Number.parseFloat(forecast.snow?.["3h"] ?? "") || 0;
176+
const precipitationAmount = rain + snow;
177+
178+
return {
179+
rain,
180+
snow,
181+
precipitationAmount,
182+
hasPrecipitation: precipitationAmount > 0
183+
};
184+
}
185+
186+
#generateHourlyWeatherObjectsFromForecast (data) {
187+
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
188+
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;
189+
190+
if (data.city?.name && data.city?.country) {
191+
this.locationName = `${data.city.name}, ${data.city.country}`;
192+
}
193+
194+
return data.list.map((forecast) => {
195+
const weather = {};
196+
weather.date = weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes);
197+
weather.temperature = forecast.main.temp;
198+
weather.feelsLikeTemp = forecast.main.feels_like;
199+
weather.humidity = forecast.main.humidity;
200+
weather.windSpeed = forecast.wind.speed;
201+
weather.windFromDirection = forecast.wind.deg;
202+
weather.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
203+
weather.precipitationProbability = forecast.pop !== undefined ? forecast.pop * 100 : undefined;
204+
205+
const precipitation = this.#extractThreeHourPrecipitation(forecast);
206+
if (precipitation.hasPrecipitation) {
207+
weather.rain = precipitation.rain;
208+
weather.snow = precipitation.snow;
209+
weather.precipitationAmount = precipitation.precipitationAmount;
210+
}
211+
212+
return weather;
213+
});
214+
}
215+
216+
#generateDailyWeatherObjectsFromForecast (data) {
217+
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
218+
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;
219+
220+
if (data.city?.name && data.city?.country) {
221+
this.locationName = `${data.city.name}, ${data.city.country}`;
222+
}
223+
224+
const dayMap = new Map();
225+
226+
for (const forecast of data.list) {
227+
// Shift dt by timezone offset so UTC fields represent local time
228+
const localDate = new Date((forecast.dt + timezoneOffsetSeconds) * 1000);
229+
const dateKey = `${localDate.getUTCFullYear()}-${String(localDate.getUTCMonth() + 1).padStart(2, "0")}-${String(localDate.getUTCDate()).padStart(2, "0")}`;
230+
231+
if (!dayMap.has(dateKey)) {
232+
dayMap.set(dateKey, {
233+
date: weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes),
234+
minTemps: [],
235+
maxTemps: [],
236+
rain: 0,
237+
snow: 0,
238+
weatherType: weatherUtils.convertWeatherType(forecast.weather[0].icon)
239+
});
240+
}
241+
242+
const day = dayMap.get(dateKey);
243+
day.minTemps.push(forecast.main.temp_min);
244+
day.maxTemps.push(forecast.main.temp_max);
245+
246+
const hour = localDate.getUTCHours();
247+
if (hour >= 8 && hour <= 17) {
248+
day.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
249+
}
250+
251+
const precipitation = this.#extractThreeHourPrecipitation(forecast);
252+
day.rain += precipitation.rain;
253+
day.snow += precipitation.snow;
254+
}
255+
256+
return Array.from(dayMap.values()).map((day) => ({
257+
date: day.date,
258+
minTemperature: Math.min(...day.minTemps),
259+
maxTemperature: Math.max(...day.maxTemps),
260+
weatherType: day.weatherType,
261+
rain: day.rain,
262+
snow: day.snow,
263+
precipitationAmount: day.rain + day.snow
264+
}));
265+
}
266+
137267
#generateWeatherObjectsFromOnecall (data) {
138268
let precip;
139269

defaultmodules/weather/weather.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Module.register("weather", {
55
defaults: {
66
weatherProvider: "openweathermap",
77
roundTemp: false,
8-
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
8+
type: "current", // current, forecast, daily (equivalent to forecast), hourly
99
lang: config.language,
1010
units: config.units,
1111
tempUnits: config.units,
@@ -242,7 +242,22 @@ Module.register("weather", {
242242

243243
// Add all the data to the template.
244244
getTemplateData () {
245-
const hourlyData = this.weatherHourlyArray?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
245+
const now = new Date();
246+
// Filter out past entries, but keep the current hour (e.g. show 0:00 at 0:10).
247+
// This ensures consistent behavior across all providers, regardless of whether
248+
// a provider filters past entries itself.
249+
const startOfHour = new Date(now);
250+
startOfHour.setMinutes(0, 0, 0);
251+
const upcomingHourlyData = this.weatherHourlyArray
252+
?.filter((entry) => entry.date?.valueOf() >= startOfHour.getTime());
253+
254+
const increment = this.config.hourlyForecastIncrements;
255+
const keepByConfiguredIncrement = (_entry, index) => {
256+
// Keep the existing offset behavior of hourlyForecastIncrements.
257+
return (index + 1) % increment === increment - 1;
258+
};
259+
260+
const hourlyData = upcomingHourlyData?.filter(keepByConfiguredIncrement);
246261

247262
return {
248263
config: this.config,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"coord": { "lon": 11.58, "lat": 48.14 },
3+
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" }],
4+
"base": "stations",
5+
"main": {
6+
"temp": -0.27,
7+
"feels_like": -3.9,
8+
"temp_min": -1.0,
9+
"temp_max": 0.5,
10+
"pressure": 1018,
11+
"humidity": 54
12+
},
13+
"visibility": 10000,
14+
"wind": { "speed": 3.09, "deg": 220 },
15+
"clouds": { "all": 100 },
16+
"dt": 1744200000,
17+
"sys": {
18+
"type": 2,
19+
"id": 2002112,
20+
"country": "DE",
21+
"sunrise": 1744170000,
22+
"sunset": 1744218000
23+
},
24+
"timezone": 7200,
25+
"id": 2867714,
26+
"name": "Munich",
27+
"cod": 200
28+
}

0 commit comments

Comments
 (0)