From e872520d4a285452b2824c366c95a5c6e26176b1 Mon Sep 17 00:00:00 2001 From: Andres Vanegas Date: Thu, 3 Nov 2022 03:55:28 -0500 Subject: [PATCH 1/4] Added a WeatherProvider for Open-Meteo --- CHANGELOG.md | 3 +- .../default/weather/providers/openmeteo.js | 549 ++++++++++++++++++ 2 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 modules/default/weather/providers/openmeteo.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 516e499a12..4583e7ebf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ This project adheres to [Semantic Versioning](https://semver.org/). _This release is scheduled to be released on 2023-01-01._ -Special thanks to: @rejas, @sdetweil, @MagMar94 +Special thanks to: @rejas, @sdetweil, @MagMar94, @angeldeejay ### Added @@ -19,6 +19,7 @@ Special thanks to: @rejas, @sdetweil, @MagMar94 - Added css class names "today" and "tomorrow" for default calendar - Added Collaboration.md - Added new github action for dependency review (#2862) +- Added a WeatherProvider for Open-Meteo. ### Removed diff --git a/modules/default/weather/providers/openmeteo.js b/modules/default/weather/providers/openmeteo.js new file mode 100644 index 0000000000..251bdd5638 --- /dev/null +++ b/modules/default/weather/providers/openmeteo.js @@ -0,0 +1,549 @@ +/* global WeatherProvider, WeatherObject */ + +/* MagicMirror² + * Module: Weather + * Provider: Open-Meteo + * + * By Andrés Vanegas + * MIT Licensed + * + * This class is a provider for Open-Meteo, based on Andrew Pometti's class + * for Weatherbit. + */ +// https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api +const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client"; + +WeatherProvider.register("openmeteo", { + // Set the name of the provider. + // Not strictly required, but helps for debugging. + providerName: "Open-Meteo", + + // Set the default config properties that is specific to this provider + defaults: { + apiBase: "https://api.open-meteo.com/v1", + lat: 0, + lon: 0, + past_days: 0, + type: "current", + timezone: null + }, + + units: { + imperial: { + temperature_unit: "fahrenheit", + windspeed_unit: "mph", + precipitation_unit: "inch" + }, + metric: { + temperature_unit: "celsius", + windspeed_unit: "kmh", + precipitation_unit: "mm" + } + }, + + // https://open-meteo.com/en/docs + hourlyParams: [ + // Air temperature at 2 meters above ground + "temperature_2m", + // Relative humidity at 2 meters above ground + "relativehumidity_2m", + // Dew point temperature at 2 meters above ground + "dewpoint_2m", + // Apparent temperature is the perceived feels-like temperature combining wind chill factor, relative humidity and solar radiation + "apparent_temperature", + // Atmospheric air pressure reduced to mean sea level (msl) or pressure at surface. Typically pressure on mean sea level is used in meteorology. Surface pressure gets lower with increasing elevation. + "pressure_msl", + "surface_pressure", + // Total cloud cover as an area fraction + "cloudcover", + // Low level clouds and fog up to 3 km altitude + "cloudcover_low", + // Mid level clouds from 3 to 8 km altitude + "cloudcover_mid", + // High level clouds from 8 km altitude + "cloudcover_high", + // Wind speed at 10, 80, 120 or 180 meters above ground. Wind speed on 10 meters is the standard level. + "windspeed_10m", + "windspeed_80m", + "windspeed_120m", + "windspeed_180m", + // Wind direction at 10, 80, 120 or 180 meters above ground + "winddirection_10m", + "winddirection_80m", + "winddirection_120m", + "winddirection_180m", + // Gusts at 10 meters above ground as a maximum of the preceding hour + "windgusts_10m", + // Shortwave solar radiation as average of the preceding hour. This is equal to the total global horizontal irradiation + "shortwave_radiation", + // Direct solar radiation as average of the preceding hour on the horizontal plane and the normal plane (perpendicular to the sun) + "direct_radiation", + "direct_normal_irradiance", + // Diffuse solar radiation as average of the preceding hour + "diffuse_radiation", + // Vapor Pressure Deificit (VPD) in kilopascal (kPa). For high VPD (>1.6), water transpiration of plants increases. For low VPD (<0.4), transpiration decreases + "vapor_pressure_deficit", + // Evapotranspration from land surface and plants that weather models assumes for this location. Available soil water is considered. 1 mm evapotranspiration per hour equals 1 liter of water per spare meter. + "evapotranspiration", + // ET₀ Reference Evapotranspiration of a well watered grass field. Based on FAO-56 Penman-Monteith equations ET₀ is calculated from temperature, wind speed, humidity and solar radiation. Unlimited soil water is assumed. ET₀ is commonly used to estimate the required irrigation for plants. + "et0_fao_evapotranspiration", + // Total precipitation (rain, showers, snow) sum of the preceding hour + "precipitation", + // Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent + "snowfall", + // Rain from large scale weather systems of the preceding hour in millimeter + "rain", + // Showers from convective precipitation in millimeters from the preceding hour + "showers", + // Weather condition as a numeric code. Follow WMO weather interpretation codes. + "weathercode", + // Snow depth on the ground + "snow_depth", + // Altitude above sea level of the 0°C level + "freezinglevel_height", + // Temperature in the soil at 0, 6, 18 and 54 cm depths. 0 cm is the surface temperature on land or water surface temperature on water. + "soil_temperature_0cm", + "soil_temperature_6cm", + "soil_temperature_18cm", + "soil_temperature_54cm", + // Average soil water content as volumetric mixing ratio at 0-1, 1-3, 3-9, 9-27 and 27-81 cm depths. + "soil_moisture_0_1cm", + "soil_moisture_1_3cm", + "soil_moisture_3_9cm", + "soil_moisture_9_27cm", + "soil_moisture_27_81cm" + ], + + dailyParams: [ + // Maximum and minimum daily air temperature at 2 meters above ground + "temperature_2m_max", + "temperature_2m_min", + // Maximum and minimum daily apparent temperature + "apparent_temperature_min", + "apparent_temperature_max", + // Sum of daily precipitation (including rain, showers and snowfall) + "precipitation_sum", + // Sum of daily rain + "rain_sum", + // Sum of daily showers + "showers_sum", + // Sum of daily snowfall + "snowfall_sum", + // The number of hours with rain + "precipitation_hours", + // The most severe weather condition on a given day + "weathercode", + // Sun rise and set times + "sunrise", + "sunset", + // Maximum wind speed and gusts on a day + "windspeed_10m_max", + "windgusts_10m_max", + // Dominant wind direction + "winddirection_10m_dominant", + // The sum of solar radiation on a given day in Megajoules + "shortwave_radiation_sum", + // Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field + "et0_fao_evapotranspiration" + ], + + fetchedLocation: function () { + return this.fetchedLocationName || ""; + }, + + fetchCurrentWeather() { + this.fetchData(this.getUrl()) + .then((data) => this.parseWeatherApiResponse(data)) + .then((parsedData) => { + if (!parsedData) { + // No usable data? + return; + } + + const currentWeather = this.generateWeatherDayFromCurrentWeather(parsedData); + this.setCurrentWeather(currentWeather); + }) + .catch(function (request) { + Log.error("Could not load data ... ", request); + }) + .finally(() => this.updateAvailable()); + }, + + fetchWeatherForecast() { + this.fetchData(this.getUrl()) + .then((data) => this.parseWeatherApiResponse(data)) + .then((parsedData) => { + if (!parsedData) { + // No usable data? + return; + } + + const dailyForecast = this.generateWeatherObjectsFromForecast(parsedData); + this.setWeatherForecast(dailyForecast); + }) + .catch(function (request) { + Log.error("Could not load data ... ", request); + }) + .finally(() => this.updateAvailable()); + }, + + fetchWeatherHourly() { + this.fetchData(this.getUrl()) + .then((data) => this.parseWeatherApiResponse(data)) + .then((parsedData) => { + if (!parsedData) { + // No usable data? + return; + } + + const hourlyForecast = this.generateWeatherObjectsFromHourly(parsedData); + this.setWeatherHourly(hourlyForecast); + }) + .catch(function (request) { + Log.error("Could not load data ... ", request); + }) + .finally(() => this.updateAvailable()); + }, + + /** + * Overrides method for setting config to check if endpoint is correct for hourly + * + * @param {object} config The configuration object + */ + setConfig(config) { + this.config = { + lang: config.lang ?? "en", + ...this.defaults, + ...config + }; + + Log.log(this.config); + + this.config.ignoreToday = (this.config.ignoreToday ?? false) && ["daily", "forecast"].includes(this.config.type); + + // Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation + const maxEntriesLimit = ["daily", "forecast"].includes(this.config.type) ? 7 : this.config.type === "hourly" ? 48 : 0; + if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) { + const daysFactor = ["daily", "forecast"].includes(this.config.type) ? 1 : this.config.type === "hourly" ? 24 : 0; + this.config.maxEntries = Math.max(1, Math.min(Math.round(parseFloat(this.config.maxNumberOfDays)) * daysFactor, maxEntriesLimit)); + this.config.maxNumberOfDays = Math.ceil(this.config.maxEntries / Math.max(1, daysFactor)); + } + this.config.maxEntries = Math.max(1, Math.min(this.config.maxEntries, maxEntriesLimit)); + + if (!this.config.hasOwnProperty("timezone") || !moment.tz.zone(this.config.timezone)) { + const validTimezone = moment.tz.guess(true); + if (this.config.hasOwnProperty("timezone")) { + Log.info(`Invalid timezone '${this.config.timezone}'. Timezone now is ${validTimezone}.`); + } else { + Log.info(`No timezone provided. Timezone now is ${validTimezone}.`); + } + this.config.timezone = validTimezone; + } + + if (!this.config.type) { + Log.error("type not configured and could not resolve it"); + } + + this.fetchLocation(); + }, + + // Generate valid query params to perform the request + getQueryParameters() { + let params = { + latitude: this.config.lat, + longitude: this.config.lon, + timeformat: "unixtime", + timezone: this.config.timezone, + past_days: this.config.past_days ?? 0, + daily: this.dailyParams, + hourly: this.hourlyParams, + ...this.units[this.config.units || "metric"] + }; + + const startDateOffset = this.config.ignoreToday ? 1 : 0; + const today = moment().startOf("day"); + const startDate = moment(today).add(startDateOffset, "days"); + + switch (this.config.type) { + case "hourly": + params["start_date"] = startDate.format("YYYY-MM-DD"); + params["end_date"] = moment(startDate).add(this.config.maxNumberOfDays, "days").format("YYYY-MM-DD"); + break; + case "daily": + case "forecast": + params["start_date"] = startDate.format("YYYY-MM-DD"); + params["end_date"] = moment(today) + .add(Math.max(0, Math.min(7, this.config.maxNumberOfDays - startDateOffset)), "days") + .format("YYYY-MM-DD"); + break; + case "current": + params["current_weather"] = true; + params["start_date"] = moment(today).format("YYYY-MM-DD"); + params["end_date"] = moment(today).format("YYYY-MM-DD"); + break; + default: + // Failsafe + return ""; + } + + return Object.keys(params) + .filter((key) => (params[key] ? true : false)) + .map((key) => { + switch (key) { + case "hourly": + case "daily": + return encodeURIComponent(key) + "=" + params[key].join(","); + default: + return encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + }) + .join("&"); + }, + + // Create a URL from the config and base URL. + getUrl() { + return `${this.config.apiBase}/forecast?${this.getQueryParameters()}`; + }, + + // Transpose hourly and daily data matrices + transposeDataMatrix(data) { + return data.time.map((_, index) => + Object.keys(data).reduce((row, key) => { + return { + ...row, + // Parse time values as momentjs instances + [key]: ["time", "sunrise", "sunset"].includes(key) ? moment.unix(data[key][index]) : data[key][index] + }; + }, {}) + ); + }, + + // Sanitize and validate API response + parseWeatherApiResponse(data) { + const validByType = { + current: data.current_weather && data.current_weather.time, + hourly: data.hourly && data.hourly.time && Array.isArray(data.hourly.time) && data.hourly.time.length > 0, + daily: data.daily && data.daily.time && Array.isArray(data.daily.time) && data.daily.time.length > 0 + }; + + if (!validByType[this.config.type]) return; + + switch (this.config.type) { + case "current": + if (!validByType.daily && !validByType.hourly) { + return; + } + break; + case "hourly": + case "forecast": + case "daily": + break; + default: + return; + } + + for (const key of ["hourly", "daily"]) { + if (typeof data[key] === "object") { + data[key] = this.transposeDataMatrix(data[key]); + } + } + + if (data.current_weather) { + data.current_weather.time = moment.unix(data.current_weather.time); + } + + return data; + }, + + // Reverse geocoding from latitude and longitude provided + fetchLocation() { + this.fetchData(`${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang}`) + .then((data) => { + if (!data || !data.city) { + // No usable data? + return; + } + this.fetchedLocationName = `${data.city}, ${data.principalSubdivisionCode}`; + }) + .catch((request) => { + Log.error("Could not load data ... ", request); + }); + }, + + // Implement WeatherDay generator. + generateWeatherDayFromCurrentWeather(weather) { + const now = moment(); + const i = 0; + const h = now.hour(); + const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + + currentWeather.date = weather.current_weather.time; + currentWeather.windSpeed = weather.current_weather.windspeed; + currentWeather.windDirection = weather.current_weather.winddirection; + currentWeather.sunrise = weather.daily[i].sunrise; + currentWeather.sunset = weather.daily[i].sunset; + currentWeather.temperature = parseFloat(weather.current_weather.temperature); + currentWeather.minTemperature = parseFloat(weather.daily[i].temperature_2m_min); + currentWeather.maxTemperature = parseFloat(weather.daily[i].temperature_2m_max); + currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime()); + currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m); + currentWeather.rain = parseFloat(weather.hourly[h].rain); + currentWeather.snow = parseFloat(weather.hourly[h].snowfall * (this.config.units === "metric" ? 10 : 1)); + currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation); + currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; + + Log.log(currentWeather); + return currentWeather; + }, + + // Implement WeatherForecast generator. + generateWeatherObjectsFromForecast(weathers) { + const days = []; + + weathers.daily.forEach((weather, i) => { + const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + + currentWeather.date = weather.time; + currentWeather.windSpeed = weather.windspeed_10m_max; + currentWeather.windDirection = weather.winddirection_10m_dominant; + currentWeather.sunrise = weather.sunrise; + currentWeather.sunset = weather.sunset; + currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2); + currentWeather.minTemperature = parseFloat(weather.apparent_temperature_min); + currentWeather.maxTemperature = parseFloat(weather.apparent_temperature_max); + currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime()); + currentWeather.rain = parseFloat(weather.rain_sum); + currentWeather.snow = parseFloat(weather.snowfall_sum * (this.config.units === "metric" ? 10 : 1)); + currentWeather.precipitation = parseFloat(weather.precipitation_sum); + currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; + + days.push(currentWeather); + }); + + return days; + }, + + // Implement WeatherHourly generator. + generateWeatherObjectsFromHourly(weathers) { + const hours = []; + const now = moment(); + + weathers.hourly.forEach((weather, i) => { + if ((hours.length === 0 && weather.time.hour() <= now.hour()) || hours.length >= this.config.maxEntries) { + return; + } + + const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); + const h = Math.ceil((i + 1) / 24) - 1; + + currentWeather.date = weather.time; + currentWeather.windSpeed = weather.windspeed_10m; + currentWeather.windDirection = weather.winddirection_10m; + currentWeather.sunrise = weathers.daily[h].sunrise; + currentWeather.sunset = weathers.daily[h].sunset; + currentWeather.temperature = parseFloat(weather.apparent_temperature); + currentWeather.minTemperature = parseFloat(weathers.daily[h].apparent_temperature_min); + currentWeather.maxTemperature = parseFloat(weathers.daily[h].apparent_temperature_max); + currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime()); + currentWeather.humidity = parseFloat(weather.relativehumidity_2m); + currentWeather.rain = parseFloat(weather.rain); + currentWeather.snow = parseFloat(weather.snowfall * (this.config.units === "metric" ? 10 : 1)); + currentWeather.precipitation = parseFloat(weather.precipitation); + currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; + + hours.push(currentWeather); + }); + + return hours; + }, + + // Map icons from Dark Sky to our icons. + convertWeatherType(weathercode, isDayTime) { + const weatherConditions = { + 0: "clear", + 1: "mainly-clear", + 2: "partly-cloudy", + 3: "overcast", + 45: "fog", + 48: "depositing-rime-fog", + 51: "drizzle-light-intensity", + 53: "drizzle-moderate-intensity", + 55: "drizzle-dense-intensity", + 56: "freezing-drizzle-light-intensity", + 57: "freezing-drizzle-dense-intensity", + 61: "rain-slight-intensity", + 63: "rain-moderate-intensity", + 65: "rain-heavy-intensity", + 66: "freezing-rain-light-heavy-intensity", + 67: "freezing-rain-heavy-intensity", + 71: "snow-fall-slight-intensity", + 73: "snow-fall-moderate-intensity", + 75: "snow-fall-heavy-intensity", + 77: "snow-grains", + 80: "rain-showers-slight", + 81: "rain-showers-moderate", + 82: "rain-showers-violent", + 85: "snow-showers-slight", + 86: "snow-showers-heavy", + 95: "thunderstorm", + 96: "thunderstorm-slight-hail", + 99: "thunderstorm-heavy-hail" + }; + + if (!Object.keys(weatherConditions).includes(`${weathercode}`)) return null; + + switch (weatherConditions[`${weathercode}`]) { + case "clear": + return isDayTime ? "day-sunny" : "night-clear"; + case "mainly-clear": + case "partly-cloudy": + return isDayTime ? "day-cloudy" : "night-alt-cloudy"; + case "overcast": + return isDayTime ? "day-sunny-overcast" : "night-alt-partly-cloudy"; + case "fog": + case "depositing-rime-fog": + return isDayTime ? "day-fog" : "night-fog"; + case "drizzle-light-intensity": + case "rain-slight-intensity": + case "rain-showers-slight": + return isDayTime ? "day-sprinkle" : "night-sprinkle"; + case "drizzle-moderate-intensity": + case "rain-moderate-intensity": + case "rain-showers-moderate": + return isDayTime ? "day-showers" : "night-showers"; + case "drizzle-dense-intensity": + case "rain-heavy-intensity": + case "rain-showers-violent": + return isDayTime ? "day-thunderstorm" : "night-thunderstorm"; + case "freezing-rain-light-intensity": + return isDayTime ? "day-rain-mix" : "night-rain-mix"; + case "freezing-drizzle-light-intensity": + case "freezing-drizzle-dense-intensity": + return "snowflake-cold"; + case "snow-grains": + return isDayTime ? "day-sleet" : "night-sleet"; + case "snow-fall-slight-intensity": + case "snow-fall-moderate-intensity": + return isDayTime ? "day-snow-wind" : "night-snow-wind"; + case "snow-fall-heavy-intensity": + case "freezing-rain-heavy-intensity": + return isDayTime ? "day-snow-thunderstorm" : "night-snow-thunderstorm"; + case "snow-showers-slight": + case "snow-showers-heavy": + return isDayTime ? "day-rain-mix" : "night-rain-mix"; + case "thunderstorm": + return isDayTime ? "day-thunderstorm" : "night-thunderstorm"; + case "thunderstorm-slight-hail": + return isDayTime ? "day-sleet" : "night-sleet"; + case "thunderstorm-heavy-hail": + return isDayTime ? "day-sleet-storm" : "night-sleet-storm"; + default: + return "na"; + } + }, + + // Define required scripts. + getScripts: function () { + return ["moment.js", "moment-timezone.js"]; + } +}); From 74601d917f838008860c2a51a66cbe345a5dd208 Mon Sep 17 00:00:00 2001 From: Andres Vanegas Date: Thu, 3 Nov 2022 17:58:14 -0500 Subject: [PATCH 2/4] FFixed suggested by @rejas --- .../default/weather/providers/openmeteo.js | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/modules/default/weather/providers/openmeteo.js b/modules/default/weather/providers/openmeteo.js index 251bdd5638..bd5ca59ce9 100644 --- a/modules/default/weather/providers/openmeteo.js +++ b/modules/default/weather/providers/openmeteo.js @@ -28,19 +28,6 @@ WeatherProvider.register("openmeteo", { timezone: null }, - units: { - imperial: { - temperature_unit: "fahrenheit", - windspeed_unit: "mph", - precipitation_unit: "inch" - }, - metric: { - temperature_unit: "celsius", - windspeed_unit: "kmh", - precipitation_unit: "mm" - } - }, - // https://open-meteo.com/en/docs hourlyParams: [ // Air temperature at 2 meters above ground @@ -217,8 +204,6 @@ WeatherProvider.register("openmeteo", { ...config }; - Log.log(this.config); - this.config.ignoreToday = (this.config.ignoreToday ?? false) && ["daily", "forecast"].includes(this.config.type); // Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation @@ -257,7 +242,10 @@ WeatherProvider.register("openmeteo", { past_days: this.config.past_days ?? 0, daily: this.dailyParams, hourly: this.hourlyParams, - ...this.units[this.config.units || "metric"] + // Fixed units as metric + temperature_unit: "celsius", + windspeed_unit: "kmh", + precipitation_unit: "mm" }; const startDateOffset = this.config.ignoreToday ? 1 : 0; @@ -372,27 +360,44 @@ WeatherProvider.register("openmeteo", { // Implement WeatherDay generator. generateWeatherDayFromCurrentWeather(weather) { - const now = moment(); - const i = 0; - const h = now.hour(); + /** + * Since some units comes from API response "splitted" into daily, hourly and current_weather + * every time you request it, you have to ensure to get the data from the right place every time. + * For the current weather case, the response have the following structure (after transposing): + * ``` + * { + * current_weather: { ... }, + * hourly: [ + * 0: {... }, + * 1: {... }, + * ... + * ], + * daily: [ + * {... }, + * ] + * } + * ``` + * Some data should be returned from `hourly` array data when the index matches the current hour, + * some data from the first and only one object received in `daily` array and some from the + * `current_weather` object. + */ + const h = moment().hour(); const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); currentWeather.date = weather.current_weather.time; currentWeather.windSpeed = weather.current_weather.windspeed; currentWeather.windDirection = weather.current_weather.winddirection; - currentWeather.sunrise = weather.daily[i].sunrise; - currentWeather.sunset = weather.daily[i].sunset; + currentWeather.sunrise = weather.daily[0].sunrise; + currentWeather.sunset = weather.daily[0].sunset; currentWeather.temperature = parseFloat(weather.current_weather.temperature); - currentWeather.minTemperature = parseFloat(weather.daily[i].temperature_2m_min); - currentWeather.maxTemperature = parseFloat(weather.daily[i].temperature_2m_max); + currentWeather.minTemperature = parseFloat(weather.daily[0].temperature_2m_min); + currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max); currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime()); currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m); currentWeather.rain = parseFloat(weather.hourly[h].rain); - currentWeather.snow = parseFloat(weather.hourly[h].snowfall * (this.config.units === "metric" ? 10 : 1)); + currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10); currentWeather.precipitation = parseFloat(weather.hourly[h].precipitation); - currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; - Log.log(currentWeather); return currentWeather; }, @@ -413,9 +418,8 @@ WeatherProvider.register("openmeteo", { currentWeather.maxTemperature = parseFloat(weather.apparent_temperature_max); currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime()); currentWeather.rain = parseFloat(weather.rain_sum); - currentWeather.snow = parseFloat(weather.snowfall_sum * (this.config.units === "metric" ? 10 : 1)); + currentWeather.snow = parseFloat(weather.snowfall_sum * 10); currentWeather.precipitation = parseFloat(weather.precipitation_sum); - currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; days.push(currentWeather); }); @@ -447,9 +451,8 @@ WeatherProvider.register("openmeteo", { currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime()); currentWeather.humidity = parseFloat(weather.relativehumidity_2m); currentWeather.rain = parseFloat(weather.rain); - currentWeather.snow = parseFloat(weather.snowfall * (this.config.units === "metric" ? 10 : 1)); + currentWeather.snow = parseFloat(weather.snowfall * 10); currentWeather.precipitation = parseFloat(weather.precipitation); - currentWeather.precipitationUnits = this.config.units === "metric" ? "mm" : "in"; hours.push(currentWeather); }); From ab7c96c7b75da86d2716498ba6c3c81a8d7db2f4 Mon Sep 17 00:00:00 2001 From: Andres Vanegas Date: Mon, 12 Dec 2022 14:52:22 -0500 Subject: [PATCH 3/4] Fixed timezone issue --- modules/default/weather/providers/openmeteo.js | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/modules/default/weather/providers/openmeteo.js b/modules/default/weather/providers/openmeteo.js index bd5ca59ce9..73a15ea101 100644 --- a/modules/default/weather/providers/openmeteo.js +++ b/modules/default/weather/providers/openmeteo.js @@ -24,8 +24,7 @@ WeatherProvider.register("openmeteo", { lat: 0, lon: 0, past_days: 0, - type: "current", - timezone: null + type: "current" }, // https://open-meteo.com/en/docs @@ -215,16 +214,6 @@ WeatherProvider.register("openmeteo", { } this.config.maxEntries = Math.max(1, Math.min(this.config.maxEntries, maxEntriesLimit)); - if (!this.config.hasOwnProperty("timezone") || !moment.tz.zone(this.config.timezone)) { - const validTimezone = moment.tz.guess(true); - if (this.config.hasOwnProperty("timezone")) { - Log.info(`Invalid timezone '${this.config.timezone}'. Timezone now is ${validTimezone}.`); - } else { - Log.info(`No timezone provided. Timezone now is ${validTimezone}.`); - } - this.config.timezone = validTimezone; - } - if (!this.config.type) { Log.error("type not configured and could not resolve it"); } @@ -238,7 +227,7 @@ WeatherProvider.register("openmeteo", { latitude: this.config.lat, longitude: this.config.lon, timeformat: "unixtime", - timezone: this.config.timezone, + timezone: "auto", past_days: this.config.past_days ?? 0, daily: this.dailyParams, hourly: this.hourlyParams, @@ -547,6 +536,6 @@ WeatherProvider.register("openmeteo", { // Define required scripts. getScripts: function () { - return ["moment.js", "moment-timezone.js"]; + return ["moment.js"]; } }); From c6e206c9f0e859c7f838fa2200e81fd75450a457 Mon Sep 17 00:00:00 2001 From: Andres Vanegas Date: Sun, 18 Dec 2022 02:55:12 -0500 Subject: [PATCH 4/4] Fixed changes requested 2022-12-12: - Forecast type bug fixed. Use of type should be working now. - Skipping offset of start date (avoiding use of in provider. --- .../default/weather/providers/openmeteo.js | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/modules/default/weather/providers/openmeteo.js b/modules/default/weather/providers/openmeteo.js index 73a15ea101..83583d5cee 100644 --- a/modules/default/weather/providers/openmeteo.js +++ b/modules/default/weather/providers/openmeteo.js @@ -12,6 +12,7 @@ */ // https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client"; +const OPEN_METEO_BASE = "https://api.open-meteo.com/v1"; WeatherProvider.register("openmeteo", { // Set the name of the provider. @@ -20,7 +21,7 @@ WeatherProvider.register("openmeteo", { // Set the default config properties that is specific to this provider defaults: { - apiBase: "https://api.open-meteo.com/v1", + apiBase: OPEN_METEO_BASE, lat: 0, lon: 0, past_days: 0, @@ -203,8 +204,6 @@ WeatherProvider.register("openmeteo", { ...config }; - this.config.ignoreToday = (this.config.ignoreToday ?? false) && ["daily", "forecast"].includes(this.config.type); - // Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation const maxEntriesLimit = ["daily", "forecast"].includes(this.config.type) ? 7 : this.config.type === "hourly" ? 48 : 0; if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) { @@ -237,26 +236,22 @@ WeatherProvider.register("openmeteo", { precipitation_unit: "mm" }; - const startDateOffset = this.config.ignoreToday ? 1 : 0; - const today = moment().startOf("day"); - const startDate = moment(today).add(startDateOffset, "days"); + const startDate = moment().startOf("day"); + const endDate = moment(startDate) + .add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days") + .endOf("day"); + + params["start_date"] = startDate.format("YYYY-MM-DD"); switch (this.config.type) { case "hourly": - params["start_date"] = startDate.format("YYYY-MM-DD"); - params["end_date"] = moment(startDate).add(this.config.maxNumberOfDays, "days").format("YYYY-MM-DD"); - break; case "daily": case "forecast": - params["start_date"] = startDate.format("YYYY-MM-DD"); - params["end_date"] = moment(today) - .add(Math.max(0, Math.min(7, this.config.maxNumberOfDays - startDateOffset)), "days") - .format("YYYY-MM-DD"); + params["end_date"] = endDate.format("YYYY-MM-DD"); break; case "current": params["current_weather"] = true; - params["start_date"] = moment(today).format("YYYY-MM-DD"); - params["end_date"] = moment(today).format("YYYY-MM-DD"); + params["end_date"] = params["start_date"]; break; default: // Failsafe @@ -302,17 +297,18 @@ WeatherProvider.register("openmeteo", { hourly: data.hourly && data.hourly.time && Array.isArray(data.hourly.time) && data.hourly.time.length > 0, daily: data.daily && data.daily.time && Array.isArray(data.daily.time) && data.daily.time.length > 0 }; + // backwards compatibility + const type = ["daily", "forecast"].includes(this.config.type) ? "daily" : this.config.type; - if (!validByType[this.config.type]) return; + if (!validByType[type]) return; - switch (this.config.type) { + switch (type) { case "current": if (!validByType.daily && !validByType.hourly) { return; } break; case "hourly": - case "forecast": case "daily": break; default: