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", () => {