diff --git a/defaultmodules/weather/current.njk b/defaultmodules/weather/current.njk index e234b9373d..a2f48589a9 100644 --- a/defaultmodules/weather/current.njk +++ b/defaultmodules/weather/current.njk @@ -35,6 +35,12 @@ {% endif %} {% endif %} + {% if config.showSunshineHours and current.sunshineHours is defined and current.sunshineHours is not none %} + + + {{ current.sunshineHours | unit("sunshine") }} + + {% endif %} {% if config.showUVIndex %}
diff --git a/defaultmodules/weather/forecast.njk b/defaultmodules/weather/forecast.njk index 047cd8b751..c57bfcd98e 100644 --- a/defaultmodules/weather/forecast.njk +++ b/defaultmodules/weather/forecast.njk @@ -32,6 +32,14 @@ {% if config.showPrecipitationProbability %}
{{ f.precipitationProbability | unit('precip', '%') }}
{% endif %} + {% if config.showSunshineHours %} +
+ {% if f.sunshineHours is defined and f.sunshineHours is not none %} + {{ f.sunshineHours | unit("sunshine") }} + + {% endif %} +
+ {% endif %}
{% set currentStep = currentStep + 1 %} {% endfor %} diff --git a/defaultmodules/weather/providers/openmeteo.js b/defaultmodules/weather/providers/openmeteo.js index 94725d80ac..bf4480a970 100644 --- a/defaultmodules/weather/providers/openmeteo.js +++ b/defaultmodules/weather/providers/openmeteo.js @@ -86,7 +86,8 @@ class OpenMeteoProvider extends WeatherProvider { "winddirection_10m_dominant", "shortwave_radiation_sum", "uv_index_max", - "et0_fao_evapotranspiration" + "et0_fao_evapotranspiration", + "sunshine_duration" ]; constructor (config) { @@ -402,6 +403,9 @@ class OpenMeteoProvider extends WeatherProvider { if (today.temperature_2m_max !== undefined) { current.maxTemperature = today.temperature_2m_max; } + if (today.sunshine_duration != null) { + current.sunshineHours = today.sunshine_duration / 3600; + } } return current; @@ -422,7 +426,8 @@ class OpenMeteoProvider extends WeatherProvider { snow: weather.snowfall_sum != null ? parseFloat(weather.snowfall_sum * 10) : null, precipitationAmount: weather.precipitation_sum != null ? parseFloat(weather.precipitation_sum) : null, precipitationProbability: weather.precipitation_hours != null ? parseFloat(weather.precipitation_hours * 100 / 24) : null, - uvIndex: weather.uv_index_max != null ? parseFloat(weather.uv_index_max) : null + uvIndex: weather.uv_index_max != null ? parseFloat(weather.uv_index_max) : null, + sunshineHours: weather.sunshine_duration != null ? weather.sunshine_duration / 3600 : null })); } diff --git a/defaultmodules/weather/weather.css b/defaultmodules/weather/weather.css index 56bb41de02..f9ebd5e686 100644 --- a/defaultmodules/weather/weather.css +++ b/defaultmodules/weather/weather.css @@ -15,7 +15,7 @@ .weather .weather-hourly { display: grid; width: max-content; - grid-template-columns: repeat(7, max-content); + grid-template-columns: repeat(8, max-content); } .weather .weather-forecast-row, @@ -43,7 +43,8 @@ .weather .precipitation-amount, .weather .precipitation-prob, .weather .humidity-hourly, -.weather .uv-index { +.weather .uv-index, +.weather .sunshine-hours { padding: 0; } @@ -79,6 +80,10 @@ order: var(--weather-forecast-precipitation-prob-order, 7); } +.weather .weather-forecast .sunshine-hours { + order: var(--weather-forecast-sunshine-hours-order, 8); +} + .weather .weather-hourly .time { order: var(--weather-hourly-time-order, 1); white-space: nowrap; diff --git a/defaultmodules/weather/weather.js b/defaultmodules/weather/weather.js index a150bd3edf..9c33a28022 100644 --- a/defaultmodules/weather/weather.js +++ b/defaultmodules/weather/weather.js @@ -25,6 +25,7 @@ Module.register("weather", { showPrecipitationProbability: false, showUVIndex: false, showSun: true, + showSunshineHours: false, showWindDirection: true, showWindDirectionAsArrow: false, degreeLabel: false, @@ -361,6 +362,8 @@ Module.register("weather", { } } else if (type === "humidity") { formattedValue = `${value}%`; + } else if (type === "sunshine") { + formattedValue = value === null || isNaN(value) ? "" : `${Math.round(value)} h`; } else if (type === "wind") { formattedValue = WeatherUtils.convertWind(value, this.config.windUnits); } diff --git a/tests/configs/modules/weather/sunshine_hours.js b/tests/configs/modules/weather/sunshine_hours.js new file mode 100644 index 0000000000..5e99e75b81 --- /dev/null +++ b/tests/configs/modules/weather/sunshine_hours.js @@ -0,0 +1,48 @@ +const config = { + address: "0.0.0.0", + ipWhitelist: [], + timeFormat: 12, + + modules: [ + { + module: "weather", + position: "bottom_bar", + config: { + lat: 48.14, + lon: 11.58, + weatherProvider: "openweathermap", + apiKey: "test-api-key", + showSunshineHours: true + } + }, + { + module: "weather", + position: "bottom_bar", + config: { + type: "forecast", + lat: 48.14, + lon: 11.58, + weatherProvider: "openweathermap", + apiKey: "test-api-key", + showSunshineHours: true + } + }, + { + module: "weather", + position: "bottom_bar", + classes: "sunshine-disabled", + config: { + type: "forecast", + lat: 48.14, + lon: 11.58, + weatherProvider: "openweathermap", + apiKey: "test-api-key" + } + } + ] +}; + +/*************** DO NOT EDIT THE LINE BELOW ***************/ +if (typeof module !== "undefined") { + module.exports = config; +} diff --git a/tests/e2e/modules/weather_sunshine_spec.js b/tests/e2e/modules/weather_sunshine_spec.js new file mode 100644 index 0000000000..04c177246f --- /dev/null +++ b/tests/e2e/modules/weather_sunshine_spec.js @@ -0,0 +1,44 @@ +const { expect } = require("playwright/test"); +const helpers = require("../helpers/global-setup"); + +describe("Weather module: sunshine hours", () => { + let page; + + beforeAll(async () => { + await helpers.startApplication("tests/configs/modules/weather/sunshine_hours.js"); + await helpers.getDocument(); + page = helpers.getPage(); + // eslint-disable-next-line playwright/no-wait-for-selector + await page.waitForSelector(".weather", { timeout: 5000 }); + + await page.evaluate(() => { + const now = Date.now(); + const day = 24 * 60 * 60 * 1000; + for (const module of MM.getModules().filter((m) => m.name === "weather")) { + const data = module.config.type === "current" + ? { date: now, temperature: 18, windSpeed: 3, sunshineHours: 4.6 } + : [0, 1].map((index) => ({ date: now + index * day, minTemperature: 8, maxTemperature: 18, weatherType: "day-sunny", sunshineHours: index ? null : 7.2 })); + module.socketNotificationReceived("WEATHER_DATA", { instanceId: module.instanceId, type: module.config.type, data }); + } + }); + }); + + afterAll(async () => { + await helpers.stopApplication(); + }); + + it("should show today's sunshine hours in the current weather", async () => { + await expect(page.locator(".weather .normal.medium .sunshine-hours")).toHaveText("5 h"); + }); + + it("should show rounded sunshine hours in the forecast", async () => { + const rows = page.locator(".weather:not(.sunshine-disabled) .weather-forecast-row"); + await expect(rows.nth(0).locator(".sunshine-hours")).toHaveText("7 h"); + await expect(rows.nth(1).locator(".sunshine-hours")).toHaveText(""); + }); + + it("should not show sunshine hours unless enabled", async () => { + await expect(page.locator(".weather.sunshine-disabled .weather-forecast-row").first()).toBeVisible(); + await expect(page.locator(".weather.sunshine-disabled .sunshine-hours")).toHaveCount(0); + }); +}); diff --git a/tests/unit/modules/default/weather/providers/openmeteo_spec.js b/tests/unit/modules/default/weather/providers/openmeteo_spec.js index 225a970c88..27377c54fe 100644 --- a/tests/unit/modules/default/weather/providers/openmeteo_spec.js +++ b/tests/unit/modules/default/weather/providers/openmeteo_spec.js @@ -252,6 +252,34 @@ describe("OpenMeteoProvider", () => { // precipitation_sum has value 0.0 in mock data expect(result[0].precipitationAmount).toBe(0.0); }); + + it("should convert sunshine duration to hours", async () => { + const provider = new OpenMeteoProvider({ + lat: 48.14, + lon: 11.58, + type: "forecast" + }); + + const dataPromise = new Promise((resolve) => { + provider.setCallbacks(resolve, vi.fn()); + }); + + const sunshineData = structuredClone(forecastData); + sunshineData.daily.sunshine_duration = sunshineData.daily.time.map((_, index) => index * 3600); + server.use( + http.get("https://api.open-meteo.com/v1/forecast*", () => { + return HttpResponse.json(sunshineData); + }) + ); + + await provider.initialize(); + provider.start(); + + const result = await dataPromise; + + expect(result[0].sunshineHours).toBe(0); + expect(result[3].sunshineHours).toBe(3); + }); }); describe("Error Handling", () => {