Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 151 additions & 21 deletions defaultmodules/weather/providers/openweathermap.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,28 +96,41 @@ class OpenWeatherMapProvider {

#handleResponse (data) {
try {
// Set location name from timezone
if (data.timezone) {
this.locationName = data.timezone;
}

let weatherData;
const onecallData = this.#generateWeatherObjectsFromOnecall(data);

switch (this.config.type) {
case "current":
weatherData = onecallData.current;
break;
case "forecast":
case "daily":
weatherData = onecallData.days;
break;
case "hourly":
weatherData = onecallData.hours;
break;
default:
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
throw new Error(`Unknown weather type: ${this.config.type}`);

if (this.config.weatherEndpoint === "/onecall") {
// One Call API (v3.0)
if (data.timezone) {
this.locationName = data.timezone;
}

const onecallData = this.#generateWeatherObjectsFromOnecall(data);

switch (this.config.type) {
case "current":
weatherData = onecallData.current;
break;
case "forecast":
case "daily":
weatherData = onecallData.days;
break;
case "hourly":
weatherData = onecallData.hours;
break;
default:
Log.error(`[openweathermap] Unknown type: ${this.config.type}`);
throw new Error(`Unknown weather type: ${this.config.type}`);
}
} else if (this.config.weatherEndpoint === "/weather") {
// Current weather endpoint (API v2.5)
weatherData = this.#generateWeatherObjectFromCurrentWeather(data);
} else if (this.config.weatherEndpoint === "/forecast") {
// 3-hourly forecast endpoint (API v2.5)
weatherData = this.config.type === "hourly"
? this.#generateHourlyWeatherObjectsFromForecast(data)
: this.#generateDailyWeatherObjectsFromForecast(data);
} else {
throw new Error(`Unknown weather endpoint: ${this.config.weatherEndpoint}`);
}

if (weatherData && this.onDataCallback) {
Expand All @@ -134,6 +147,123 @@ class OpenWeatherMapProvider {
}
}

#generateWeatherObjectFromCurrentWeather (data) {
const timezoneOffsetMinutes = (data.timezone ?? 0) / 60;

if (data.name && data.sys?.country) {
this.locationName = `${data.name}, ${data.sys.country}`;
} else if (data.name) {
this.locationName = data.name;
}

const weather = {};
weather.date = weatherUtils.applyTimezoneOffset(new Date(data.dt * 1000), timezoneOffsetMinutes);
weather.temperature = data.main.temp;
weather.feelsLikeTemp = data.main.feels_like;
weather.humidity = data.main.humidity;
weather.windSpeed = data.wind.speed;
weather.windFromDirection = data.wind.deg;
weather.weatherType = weatherUtils.convertWeatherType(data.weather[0].icon);
weather.sunrise = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunrise * 1000), timezoneOffsetMinutes);
weather.sunset = weatherUtils.applyTimezoneOffset(new Date(data.sys.sunset * 1000), timezoneOffsetMinutes);

return weather;
}

#extractThreeHourPrecipitation (forecast) {
const rain = Number.parseFloat(forecast.rain?.["3h"] ?? "") || 0;
const snow = Number.parseFloat(forecast.snow?.["3h"] ?? "") || 0;
const precipitationAmount = rain + snow;

return {
rain,
snow,
precipitationAmount,
hasPrecipitation: precipitationAmount > 0
};
}

#generateHourlyWeatherObjectsFromForecast (data) {
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;

if (data.city?.name && data.city?.country) {
this.locationName = `${data.city.name}, ${data.city.country}`;
}

return data.list.map((forecast) => {
const weather = {};
weather.date = weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes);
weather.temperature = forecast.main.temp;
weather.feelsLikeTemp = forecast.main.feels_like;
weather.humidity = forecast.main.humidity;
weather.windSpeed = forecast.wind.speed;
weather.windFromDirection = forecast.wind.deg;
weather.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
weather.precipitationProbability = forecast.pop !== undefined ? forecast.pop * 100 : undefined;

const precipitation = this.#extractThreeHourPrecipitation(forecast);
if (precipitation.hasPrecipitation) {
weather.rain = precipitation.rain;
weather.snow = precipitation.snow;
weather.precipitationAmount = precipitation.precipitationAmount;
}

return weather;
});
}

#generateDailyWeatherObjectsFromForecast (data) {
const timezoneOffsetSeconds = data.city?.timezone ?? 0;
const timezoneOffsetMinutes = timezoneOffsetSeconds / 60;

if (data.city?.name && data.city?.country) {
this.locationName = `${data.city.name}, ${data.city.country}`;
}

const dayMap = new Map();

for (const forecast of data.list) {
// Shift dt by timezone offset so UTC fields represent local time
const localDate = new Date((forecast.dt + timezoneOffsetSeconds) * 1000);
const dateKey = `${localDate.getUTCFullYear()}-${String(localDate.getUTCMonth() + 1).padStart(2, "0")}-${String(localDate.getUTCDate()).padStart(2, "0")}`;

if (!dayMap.has(dateKey)) {
dayMap.set(dateKey, {
date: weatherUtils.applyTimezoneOffset(new Date(forecast.dt * 1000), timezoneOffsetMinutes),
minTemps: [],
maxTemps: [],
rain: 0,
snow: 0,
weatherType: weatherUtils.convertWeatherType(forecast.weather[0].icon)
});
}

const day = dayMap.get(dateKey);
day.minTemps.push(forecast.main.temp_min);
day.maxTemps.push(forecast.main.temp_max);

const hour = localDate.getUTCHours();
if (hour >= 8 && hour <= 17) {
day.weatherType = weatherUtils.convertWeatherType(forecast.weather[0].icon);
}

const precipitation = this.#extractThreeHourPrecipitation(forecast);
day.rain += precipitation.rain;
day.snow += precipitation.snow;
}

return Array.from(dayMap.values()).map((day) => ({
date: day.date,
minTemperature: Math.min(...day.minTemps),
maxTemperature: Math.max(...day.maxTemps),
weatherType: day.weatherType,
rain: day.rain,
snow: day.snow,
precipitationAmount: day.rain + day.snow
}));
}

#generateWeatherObjectsFromOnecall (data) {
let precip;

Expand Down
20 changes: 18 additions & 2 deletions defaultmodules/weather/weather.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Module.register("weather", {
defaults: {
weatherProvider: "openweathermap",
roundTemp: false,
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
type: "current", // current, forecast, daily (equivalent to forecast), hourly
lang: config.language,
units: config.units,
tempUnits: config.units,
Expand Down Expand Up @@ -242,7 +242,23 @@ Module.register("weather", {

// Add all the data to the template.
getTemplateData () {
const hourlyData = this.weatherHourlyArray?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
const now = new Date();
// Filter out past entries, but keep the current hour (e.g. show 0:00 at 0:10).
// This ensures consistent behavior across all providers, regardless of whether
// a provider filters past entries itself.
const startOfHour = new Date(now);
startOfHour.setMinutes(0, 0, 0);
const upcomingHourlyData = this.weatherHourlyArray
?.filter((entry) => entry.date?.valueOf() >= startOfHour.getTime());
const hourlySourceData = upcomingHourlyData?.length ? upcomingHourlyData : this.weatherHourlyArray;

const increment = this.config.hourlyForecastIncrements;
const keepByConfiguredIncrement = (_entry, index) => {
// Keep the existing offset behavior of hourlyForecastIncrements.
return (index + 1) % increment === increment - 1;
};

const hourlyData = hourlySourceData?.filter(keepByConfiguredIncrement);

return {
config: this.config,
Expand Down
28 changes: 28 additions & 0 deletions tests/mocks/weather_owm_current.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"coord": { "lon": 11.58, "lat": 48.14 },
"weather": [{ "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" }],
"base": "stations",
"main": {
"temp": -0.27,
"feels_like": -3.9,
"temp_min": -1.0,
"temp_max": 0.5,
"pressure": 1018,
"humidity": 54
},
"visibility": 10000,
"wind": { "speed": 3.09, "deg": 220 },
"clouds": { "all": 100 },
"dt": 1744200000,
"sys": {
"type": 2,
"id": 2002112,
"country": "DE",
"sunrise": 1744170000,
"sunset": 1744218000
},
"timezone": 7200,
"id": 2867714,
"name": "Munich",
"cod": 200
}
Loading
Loading