Skip to content

[weather] refactor: migrate to server-side providers with centralized HTTPFetcher#35

Open
KristjanESPERANTO wants to merge 107 commits intodevelopfrom
weather
Open

[weather] refactor: migrate to server-side providers with centralized HTTPFetcher#35
KristjanESPERANTO wants to merge 107 commits intodevelopfrom
weather

Conversation

@KristjanESPERANTO
Copy link
Owner

@KristjanESPERANTO KristjanESPERANTO commented Feb 2, 2026

This migrates the Weather module from client-side fetching to use the server-side centralized HTTPFetcher (introduced in MagicMirrorOrg#4016), following the same pattern as the Calendar and Newsfeed modules.

Motivation

This brings consistent error handling and better maintainability and completes the refactoring effort to centralize HTTP error handling across all default modules.

Migrating to server-side providers with HTTPFetcher brings:

  • Centralized error handling: Inherits smart retry strategies (401/403, 429, 5xx backoff) and timeout handling (30s)
  • Consistency: Same architecture as Calendar and Newsfeed modules
  • Improved tests

Changes

  • All 10 weather providers now use HTTPFetcher for server-side fetching
  • Consistent error handling like Calendar and Newsfeed modules

Breaking Changes

None. Existing configurations continue to work.

Testing

To ensure proper functionality, I obtained API keys and credentials for all providers that require them. I configured all 10 providers in a carousel setup and tested each one individually. Screenshots for each provider are attached below demonstrating their working state.

I even requested developer access from the Tempest/WeatherFlow team to properly test their provider.

Comprehensive test coverage: A major advantage of the server-side architecture is the ability to thoroughly test providers with unit tests using real API response snapshots. Don't be alarmed by the many lines added in this PR - they are primarily test files and real-data mocks that ensure provider reliability.

Related

Part of the HTTPFetcher migration MagicMirrorOrg#4016.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch weather

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@KristjanESPERANTO KristjanESPERANTO changed the title refactor: migrate weather module to server-side providers [weather] refactor: migrate to server-side providers with centralized HTTPFetcher Feb 2, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Fix all issues with AI agents
In `@defaultmodules/weather/node_helper.js`:
- Around line 38-42: The code uses the user-derived identifier directly to build
providerPath and require(providerPath), which allows path traversal;
validate/sanitize identifier before use by enforcing a strict allowlist or
whitelist-regex (e.g., only alphanumeric, dash, underscore) and/or map known
provider names to filenames, then resolve the final path and verify it is inside
the providers directory (use path.resolve on providersDir and providerPath and
assert the resolved providerPath startsWith the resolved providersDir) before
calling require(providerPath); update references in node_helper.js (identifier,
providerPath, require(providerPath)) to perform these checks and fall back to a
safe default or throw a controlled error on invalid input.

In `@defaultmodules/weather/providers/openmeteo.js`:
- Around line 205-231: The switch over this.config.type can leave weatherData
undefined for unknown values; update the switch in the try block (the switch
that calls `#generateWeatherDayFromCurrentWeather`,
`#generateWeatherObjectsFromForecast`, `#generateWeatherObjectsFromHourly`) to
include a default case that either sets weatherData to a sensible fallback
(e.g., empty object/array) or throws a descriptive Error; if you choose to
throw, catch will log via Log.error and invoke this.onErrorCallback({ message:
error.message, translationKey: "MODULE_ERROR_UNSPECIFIED" }); ensure you do not
call this.onDataCallback with undefined by validating weatherData before calling
this.onDataCallback.
- Around line 411-434: The function `#generateWeatherDayFromCurrentWeather` uses
new Date().getHours() which is the server local hour and can mismatch the API
timezone; instead derive the correct hourly index from
parsedData.current_weather.time by finding that timestamp in
parsedData.hourly.time (e.g., use
parsedData.hourly.time.indexOf(parsedData.current_weather.time) or a findIndex
that normalizes formats) and use that index for parsedData.hourly[...] lookups
(ensure you fall back safely if index is -1); update references to h accordingly
so humidity, feelsLikeTemp, rain, snow, precipitationAmount,
precipitationProbability and uvIndex use the location-correct hourly entry.

In `@defaultmodules/weather/providers/openweathermap.js`:
- Around line 30-43: The initialize() method currently assumes onErrorCallback
exists and may silently swallow the error if setCallbacks() hasn't run; change
initialize() to validate that onErrorCallback is a function (e.g., typeof
this.onErrorCallback === 'function') before invoking it and, if it's missing,
throw an Error (or return a rejected Promise) with the same message so the
caller is notified; update the error handling block in initialize() (and any
helper like `#initializeFetcher`() if it relies on callbacks) to either call the
callback when present or throw/reject when it's absent, and document in comments
that setCallbacks() must be called before initialize() if you prefer keeping
callback-driven behavior.
- Around line 100-127: The switch over this.config.type in the try block (using
this.#generateWeatherObjectsFromOnecall and assigning weatherData) lacks a
default branch, so unknown types can leave weatherData undefined and pass that
to this.onDataCallback; add a default case that assigns a safe fallback (e.g.,
an empty object/array or onecallData.current) and/or logs an error before
invoking this.onDataCallback, and ensure the same behavior routes through
this.onErrorCallback if you prefer to treat unknown types as errors.

In `@defaultmodules/weather/providers/weatherflow.js`:
- Around line 13-16: The constructor currently assigns the incoming config
directly (constructor, this.config) which can leave keys like type and
updateInterval undefined; update the constructor to merge the provided config
with a sensible defaults object (e.g., defaults for type, updateInterval,
apiKey/endpoint as appropriate) and assign the merged result to this.config, and
ensure any dependent initialization (like this.fetcher creation) uses the merged
this.config values so missing fields get defaulted rather than causing undefined
behavior.
- Line 2: The import in weatherflow.js uses a relative path for HTTPFetcher;
replace the require("../../../js/http_fetcher") with the module alias
require("#http_fetcher") so HTTPFetcher is imported consistently with other
providers (locate the const HTTPFetcher declaration in weatherflow.js and update
its require to "#http_fetcher").
- Around line 258-268: The provider exposes two separate setters
setOnDataCallback and setOnErrorCallback which is inconsistent with other
providers that implement a single setCallbacks(onData, onError) API expected by
the node_helper; change the WeatherFlow provider to implement a single method
named setCallbacks(onData, onError) that assigns both this.onDataCallback and
this.onErrorCallback and keep or alias the existing
setOnDataCallback/setOnErrorCallback to call the new setCallbacks to preserve
backward compatibility, ensuring callers (and node_helper) can use the uniform
setCallbacks interface.
- Around line 62-64: The provider currently attaches fetcher.on("data", ...)
which never fires; change it to listen for the "response" event from HTTPFetcher
(fetcher.on("response", ...)), await response.json() and then pass the parsed
JSON into this.processData and this.onDataCallback, i.e. replace the "data"
handler with an async "response" handler that calls response.json() before
invoking this.processData(...) and this.onDataCallback(...).

In `@defaultmodules/weather/providers/weathergov.js`:
- Around line 303-309: The forecast.windDirection is a cardinal string (e.g.,
"NW") but weather.windFromDirection is expected to be numeric degrees; add a
private helper (e.g., `#convertWindDirection`(direction)) that maps N, NNE, NE,
... NNW to their degree values (N=0, NNE=22.5, NE=45, …, NNW=337.5) and call it
where you set weather.windFromDirection instead of assigning
forecast.windDirection directly; if the mapping returns undefined/unknown, set
windFromDirection to null (or leave as original only if you intentionally need
the string) and ensure callers of this field handle numeric or null values.

In `@defaultmodules/weather/providers/yr.js`:
- Around line 440-444: The getSunriseUrl function currently hardcodes
offset=+01:00 which breaks sunrise times outside CET; update getSunriseUrl to
use a configurable or computed timezone offset instead of the literal.
Specifically, read an offset value from this.config.offset (if present) and
otherwise compute the local offset for the device (e.g., use the local
Date.getTimezoneOffset for today and convert to the ±HH:MM format) and include
that formatted value in the URL (or omit the offset parameter when you prefer
UTC). Modify the `#getSunriseUrl` method to use this.config.offsetFallback (or
computed value) in place of the hardcoded "+01:00" so different timezones get
correct sunrise/sunset times.

In `@defaultmodules/weather/weather.js`:
- Around line 195-205: In createWeatherObject, avoid calling moment() with
undefined/null for sunrise and sunset (which yields current time); update
createWeatherObject (and the WeatherObject assignment) to only convert
date/sunrise/sunset with moment when the source values are non-null/defined
(e.g., date: data.date ? moment(data.date) : null, sunrise: data.sunrise ?
moment(data.sunrise) : null, sunset: data.sunset ? moment(data.sunset) : null)
so missing values remain null/undefined instead of becoming the current time.
🧹 Nitpick comments (13)
tests/unit/modules/default/weather/weather_providers_spec.js (1)

9-13: Consider restoring the global fetch mock between tests.

The global fetch mock is set once and never restored. While this may work for these smoke tests, it could leak into other test files if they run in the same process context.

♻️ Suggested improvement
+import { afterAll } from "vitest";
+
+const originalFetch = global.fetch;
+
 // Mock global fetch for location lookup
 global.fetch = vi.fn(() => Promise.resolve({
 	ok: true,
 	json: () => Promise.resolve({ city: "Munich", locality: "Munich" })
 }));
+
+afterAll(() => {
+	global.fetch = originalFetch;
+});
tests/electron/helpers/weather-setup.js (2)

118-129: Replace waitForTimeout with proper wait conditions.

Static analysis correctly flags page.waitForTimeout() as an anti-pattern. These fixed delays are flaky and slow. Consider waiting for specific DOM elements or module states instead.

♻️ Suggested improvement
 exports.startApp = async (configFileName, systemDate, mockDataFile = "weather_onecall_current.json") => {
 	await helpers.startApplication(configFileName, systemDate);
 
-	// Wait for modules to initialize
-	await global.page.waitForTimeout(1000);
+	// Wait for weather module to be present in DOM
+	await global.page.waitForSelector(".module.weather", { state: "attached" });
 
 	// Inject mock weather data
 	await injectMockWeatherData(mockDataFile);
 
-	// Wait for rendering
-	await global.page.waitForTimeout(500);
+	// Wait for weather data to render
+	await global.page.waitForSelector(".weather .normal.medium", { state: "visible" });
 };

14-42: Consider extracting shared test utilities.

The convertWeatherType and applyOffset helpers are duplicated between this file and tests/e2e/helpers/weather-functions.js. Extracting these to a shared module would reduce maintenance burden.

tests/e2e/helpers/weather-functions.js (2)

117-124: Replace waitForTimeout with proper wait conditions.

Static analysis correctly flags page.waitForTimeout() as an anti-pattern. These fixed delays make tests flaky and slower than necessary.

♻️ Suggested improvement
 	// If mock data file is provided, inject it
 	if (mockDataFile) {
 		const page = helpers.getPage();
-		// Wait for modules to initialize
-		await page.waitForTimeout(1000);
+		// Wait for weather module to be present
+		await page.waitForSelector(".module.weather", { state: "attached" });
 		await injectMockWeatherData(page, mockDataFile);
-		// Wait for rendering
-		await page.waitForTimeout(500);
+		// Wait for weather content to render
+		await page.waitForSelector(".weather .normal.medium", { state: "visible" });
 	}

92-110: Consider adding error handling when weather module is not found.

If weatherModule is null, the injection silently does nothing. This could mask test failures. Consider throwing an error or returning a status.

♻️ Suggested improvement
 	// Inject weather data by evaluating code in the browser context
-	await page.evaluate(({ type, data }) => {
+	const injected = await page.evaluate(({ type, data }) => {
 		// Find the weather module instance
 		const weatherModule = MM.getModules().find((m) => m.name === "weather");
 		if (weatherModule) {
 			// Send INITIALIZED first
 			weatherModule.socketNotificationReceived("WEATHER_INITIALIZED", {
 				instanceId: weatherModule.instanceId,
 				locationName: "Munich"
 			});
 			// Then send the actual data
 			weatherModule.socketNotificationReceived("WEATHER_DATA", {
 				instanceId: weatherModule.instanceId,
 				type: type,
 				data: data
 			});
+			return true;
 		}
+		return false;
 	}, { type, data });
+
+	if (!injected) {
+		throw new Error("Weather module not found - cannot inject mock data");
+	}
defaultmodules/weather/providers/envcanada.js (2)

29-29: Consider using null instead of magic number 999 for temperature cache.

Using 999 as a sentinel value could be problematic if displayed to users when no valid temperature has been cached yet. A null value would be more explicit and easier to handle in downstream code.

Suggested fix
-		this.cacheCurrentTemp = 999;
+		this.cacheCurrentTemp = null;

Then update the usage in #generateCurrentWeather:

 		if (temp && temp !== "") {
 			current.temperature = parseFloat(temp);
 			this.cacheCurrentTemp = current.temperature;
-		} else {
+		} else if (this.cacheCurrentTemp !== null) {
 			current.temperature = this.cacheCurrentTemp;
 		}

340-351: Silent fallback to current date may mask data issues.

When timeStr is invalid or too short, returning new Date() silently could lead to incorrect weather data timestamps. Consider returning null and handling it in the caller, or at least logging a warning.

Suggested improvement
 	`#parseECTime` (timeStr) {
-		if (!timeStr || timeStr.length < 14) return new Date();
+		if (!timeStr || timeStr.length < 14) {
+			Log.warn("[weatherprovider.envcanada] Invalid time string:", timeStr);
+			return null;
+		}
defaultmodules/weather/providers/yr.js (1)

199-202: Consider awaiting stellar data refresh or documenting the async behavior.

When using cached weather data, #fetchStellarData() is called without await. This fire-and-forget approach means the current response will use potentially stale stellar data. If immediate accuracy is needed, consider awaiting the refresh.

-			if (fromCache) {
-				this.#fetchStellarData();
-			}
+			if (fromCache) {
+				await this.#fetchStellarData();
+			}
defaultmodules/weather/providers/weathergov.js (1)

291-291: Unconditionally skipping the first day may lose valid forecast data.

The comment says "Skip first incomplete day," but depending on when the forecast is fetched, the first day might contain complete data. Consider checking if the first day is actually incomplete before skipping.

defaultmodules/weather/providers/smhi.js (1)

64-70: Potential locale issue with Intl.NumberFormat parsing.

Using Intl.NumberFormat with en-US locale ensures decimal points, but parseFloat may still have issues if the formatted string contains grouping separators. For coordinate precision, consider using toFixed() instead:

Alternative implementation
 	`#limitDecimals` (value, decimals) {
-		const formatter = new Intl.NumberFormat("en-US", {
-			minimumFractionDigits: decimals,
-			maximumFractionDigits: decimals
-		});
-		return parseFloat(formatter.format(value));
+		return parseFloat(parseFloat(value).toFixed(decimals));
 	}
defaultmodules/weather/providers/ukmetofficedatahub.js (1)

53-105: Consider making internal methods private for encapsulation.

Several methods that are implementation details are exposed as public: initializeFetcher, getForecastType, getUrl, handleResponse, generateCurrentWeather, generateForecast, generateHourly, and convertWeatherType. Other providers in this PR use private methods (prefixed with #) for these.

Example refactor for key methods
-	initializeFetcher () {
+	`#initializeFetcher` () {

-	getForecastType () {
+	`#getForecastType` () {

-	getUrl (forecastType) {
+	`#getUrl` (forecastType) {

-	handleResponse (data) {
+	`#handleResponse` (data) {

Then update initialize():

-		this.initializeFetcher();
+		this.#initializeFetcher();
defaultmodules/weather/providers/openmeteo.js (2)

234-244: Redundant hasOwnProperty check.

Since maxNumberOfDays is always present in this.config from the constructor defaults (line 99), the hasOwnProperty check is unnecessary.

♻️ Simplified version
-		if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) {
+		if (!isNaN(parseFloat(this.config.maxNumberOfDays))) {
 			const daysFactor = ["daily", "forecast"].includes(this.config.type) ? 1 : this.config.type === "hourly" ? 24 : 0;

370-370: Consider using in operator for object key check.

Object.keys(weatherConditions).includes(\${weathercode}`)creates an array on every call. Using thein` operator is more efficient.

♻️ Proposed fix
-		if (!Object.keys(weatherConditions).includes(`${weathercode}`)) return null;
+		if (!(weathercode in weatherConditions)) return null;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/unit/functions/server_functions_spec.js (1)

3-19: ⚠️ Potential issue | 🟡 Minor

Restore global.config to avoid test leakage.
Mutating global state without cleanup can spill into other tests.

🛠️ Suggested fix
 describe("server_functions tests", () => {
 	describe("getUserAgent", () => {
 		it("Gets User-Agent from configuration", async () => {
-			global.config = {};
+			const previousConfig = global.config;
+			global.config = {};
 			let userAgent;
 
 			userAgent = getUserAgent();
 			expect(userAgent).toContain("Mozilla/5.0 (Node.js ");
@@
 			global.config.userAgent = () => "Mozilla/5.0 (Bar)";
 			userAgent = getUserAgent();
 			expect(userAgent).toBe("Mozilla/5.0 (Bar)");
+
+			global.config = previousConfig;
 		});
 	});
 });
🤖 Fix all issues with AI agents
In `@defaultmodules/weather/providers/envcanada.js`:
- Around line 296-301: The code is constructing weather.date by calling
this.#parseECTime(dateTimeUTC) which yields a local-time Date and then adds
utcOffset, causing a double shift; change parsing so dateTimeUTC is interpreted
as UTC before applying utcOffset (e.g., adjust this.#parseECTime to return a
Date in UTC or normalize dateTimeUTC to include a 'Z'/use Date.UTC/Date.parse
with UTC components), then compute weather.date = new Date(utcTime.getTime() +
utcOffset * 60 * 60 * 1000) so the offset is applied only once; update
references in the loop over hourlyMatches (dateTimeUTC, this.#parseECTime,
utcOffset, weather.date) accordingly.

In `@defaultmodules/weather/providers/openmeteo.js`:
- Around line 425-441: parsedData.hourly can be either an object with arrays or
an array of hourly objects (after transpose), so the current block skips because
parsedData.hourly.time is undefined and the time comparison may be by reference;
update the enrichment in the openmeteo provider to handle both shapes: if
parsedData.hourly.time exists use it as before, otherwise if
Array.isArray(parsedData.hourly) find the index with
parsedData.hourly.findIndex(h => String(h.time) ===
String(parsedData.current_weather.time)); use that index (or 0 if not found,
logging the warning) and then read values either from
parsedData.hourly.<field>[index] (object-with-arrays case) or
parsedData.hourly[index]?.<field> (array-of-objects case) for fields like
relativehumidity_2m, apparent_temperature, rain, snowfall, precipitation,
precipitation_probability, uv_index and set
current.humidity/current.feelsLikeTemp/etc. accordingly.
- Around line 284-286: The current Object.keys(params).filter((key) =>
!!params[key])... removes valid numeric zeros (latitude/longitude); update the
filter to preserve 0 by checking for null/undefined/empty-string instead, e.g.
use .filter((key) => params[key] !== undefined && params[key] !== null &&
params[key] !== '') so keys with value 0 remain included when building the query
(locate the Object.keys(params).filter(...).map(...) block in openmeteo.js).

In `@defaultmodules/weather/providers/smhi.js`:
- Around line 54-62: The validateConfig() method currently treats 0 as falsy and
rejects valid coordinates; update the check to only reject missing coordinates
by testing for null/undefined (e.g., use this.config.lat == null ||
this.config.lon == null or typeof checks) instead of a falsy check, then
continue to apply limitDecimals(this.config.lat, 6) and
limitDecimals(this.config.lon, 6) as before so zero coordinates are accepted but
absent values still throw the Error("Latitude and longitude are required").
- Around line 278-296: The gap-filling currently clones data[i] and generates
hourly points starting at the previous timestamp, which shifts values and never
re-adds the real entries; update `#fillInGaps`(data) to start by pushing the first
original point (data[0]), then for each interval between data[i-1] and data[i]
clone data[i-1] (not data[i]) to generate hourly entries from the previous
timestamp up to but not including the next timestamp (set validTime via new
Date(from).setHours(from.getHours()+j) and toISOString()), and finally push the
actual data[i] after filling the hourly gaps so original points are preserved.
Ensure types and ISO formatting remain consistent.

In `@defaultmodules/weather/providers/ukmetofficedatahub.js`:
- Around line 149-254: The code uses the key precipitation but the templates
expect precipitationAmount; update all objects to use precipitationAmount
instead of precipitation: change the current object built in the current-weather
logic (the first current with precipitation: (hour.totalPrecipAmount || 0) +
(hour.totalSnowAmount || 0)), the fallback current (firstHour case), each day
object returned in `#generateForecast` (set precipitationAmount appropriately,
e.g. combine rain/snow or use provided probability/amount fields), and each hour
object in `#generateHourly` (replace precipitation with precipitationAmount and
assign (hour.totalPrecipAmount || 0) + (hour.totalSnowAmount || 0)). Keep the
original fallback/default numeric logic (|| 0) and do not change other field
names or calculations.

In `@defaultmodules/weather/providers/weathergov.js`:
- Around line 66-120: The two native fetch calls inside the private method
`#fetchWeatherGovURLs` lack timeouts; add an AbortController with a configurable
timeout (e.g., this.config.fetchTimeout) for each fetch, pass controller.signal
to fetch(this.observationStationsURL, { ..., signal }) and the pointsUrl fetch,
set a timer to call controller.abort() after the timeout, clear the timer on
success, and catch/translate AbortError into a clear error (e.g., throw new
Error("Weather.gov request timed out while fetching grid point" or "observation
stations")). Ensure the AbortController is created per request (or reused
carefully) and that the signal is included in both fetch calls so initialization
cannot hang indefinitely.
- Around line 342-345: The code incorrectly converts observation wind speeds
because Weather.gov observations are already in m/s but the parser calls the
private helper `#convertWindToMs`; remove the call to `#convertWindToMs` when
processing observation data (only use it for forecast data returned in km/h when
?units=si), or alternatively make `#convertWindToMs` unit-aware by accepting a
units flag (e.g., 'm/s' vs 'km/h') and only dividing by 3.6 for km/h; update the
observation parsing path to pass the correct units or skip conversion so
observed values remain in m/s.

In `@defaultmodules/weather/providers/yr.js`:
- Around line 194-207: The switch over this.config.type can fall through and
leave weatherData undefined for unknown values; add a default branch to the
switch that handles unexpected types (e.g. in the switch in yr.js add a default:
case that throws or returns an Error/Promise rejection with a clear message like
"Unknown weather type: <value>" or calls the existing callback with an Error) so
that `#generateCurrentWeather`, `#generateForecast` and `#generateHourly` are only
used for known types and callers get an explicit error when this.config.type is
invalid.
- Around line 72-79: The validation in the private method `#validateConfig`
rejects valid 0 coordinates because it uses a falsy check; change the check to
explicitly validate presence and numericness (e.g., ensure lat/lon are not
null/undefined and are finite numbers) before calling
limitDecimals(this.config.lat, 4) and limitDecimals(this.config.lon, 4), and
throw the same error message if validation fails; reference the `#validateConfig`
method and the use of limitDecimals to locate where to update the condition.

In `@tests/e2e/helpers/weather-functions.js`:
- Around line 15-59: The helper currently leaves data null when rawData lacks
current, daily, or hourly, causing silent invalid payloads; add a guard in the
weather mock (use rawData, type, data) that immediately throws a clear Error (or
asserts) when none of rawData.current, rawData.daily, or rawData.hourly are
present—include the offending rawData keys or a brief description in the error
message so tests fail fast and show which fixture format is unsupported.

In `@tests/unit/modules/default/weather/weather_providers_spec.js`:
- Around line 1-8: Replace the ESM import line with a CommonJS require call so
the Vitest symbols are loaded via const destructuring; specifically change the
top-level `import { describe, it, expect, vi, beforeEach, beforeAll, afterAll }
from "vitest";` to use `require()` and keep the same symbol names (`describe`,
`it`, `expect`, `vi`, `beforeEach`, `beforeAll`, `afterAll`) so the rest of the
test file continues to work unchanged.
🧹 Nitpick comments (4)
defaultmodules/weather/provider-utils.js (2)

70-77: Consider handling polar edge cases.

The suncalc library returns Invalid Date for polar regions during perpetual day/night. Callers like isDayTime handle null values but not Invalid Date. Consider adding validation:

♻️ Optional: Handle invalid dates from polar regions
 function getSunTimes (date, lat, lon) {
 	const SunCalc = require("suncalc");
 	const sunTimes = SunCalc.getTimes(date, lat, lon);
+	const isValidDate = (d) => d instanceof Date && !isNaN(d.getTime());
 	return {
-		sunrise: sunTimes.sunrise,
-		sunset: sunTimes.sunset
+		sunrise: isValidDate(sunTimes.sunrise) ? sunTimes.sunrise : null,
+		sunset: isValidDate(sunTimes.sunset) ? sunTimes.sunset : null
 	};
 }

110-112: Clarify UTC behavior in documentation.

toISOString() converts to UTC before formatting, which may return a different date than the local date near midnight. If this is intentional, consider updating the JSDoc to note the UTC behavior.

📝 Optional: Clarify UTC behavior or use local date

If UTC is intentional:

 /**
- * Get date string in YYYY-MM-DD format
+ * Get date string in YYYY-MM-DD format (UTC)
  * `@param` {Date} date - The date to format
  * `@returns` {string} Date string in YYYY-MM-DD format
  */

If local date is preferred:

 function getDateString (date) {
-	return date.toISOString().split("T")[0];
+	const year = date.getFullYear();
+	const month = String(date.getMonth() + 1).padStart(2, "0");
+	const day = String(date.getDate()).padStart(2, "0");
+	return `${year}-${month}-${day}`;
 }
tests/electron/helpers/weather-setup.js (1)

88-99: Hardcoded timeouts may cause test flakiness.

Using waitForTimeout with fixed delays (1000ms, 500ms) can lead to flaky tests on slower CI environments. Consider using waitForSelector or waitForFunction to wait for specific conditions instead.

♻️ Suggested improvement: Use condition-based waiting
 exports.startApp = async (configFileName, systemDate, mockDataFile = "weather_onecall_current.json") => {
 	await helpers.startApplication(configFileName, systemDate);

 	// Wait for modules to initialize
-	await global.page.waitForTimeout(1000);
+	await global.page.waitForFunction(() => {
+		return typeof MM !== 'undefined' && MM.getModules().some((m) => m.name === "weather");
+	}, { timeout: 5000 });

 	// Inject mock weather data
 	await injectMockWeatherData(mockDataFile);

 	// Wait for rendering
-	await global.page.waitForTimeout(500);
+	await global.page.waitForSelector('.weather', { timeout: 5000 });
 };
defaultmodules/weather/providers/weatherbit.js (1)

146-159: Edge case: Sunrise/sunset date assumption.

The sunrise/sunset parsing uses new Date() (today) as the base, which could be incorrect near midnight if the API returns yesterday's sunset time. This is a minor edge case but worth noting.

📝 Optional: Consider using data timestamp for date base
 	// Parse sunrise/sunset from HH:mm format (already in local time)
 	if (current.sunrise) {
 		const [hours, minutes] = current.sunrise.split(":");
-		const sunrise = new Date();
+		const sunrise = new Date(current.ts * 1000);
 		sunrise.setHours(parseInt(hours), parseInt(minutes), 0, 0);
 		weather.sunrise = sunrise;
 	}

 	if (current.sunset) {
 		const [hours, minutes] = current.sunset.split(":");
-		const sunset = new Date();
+		const sunset = new Date(current.ts * 1000);
 		sunset.setHours(parseInt(hours), parseInt(minutes), 0, 0);
 		weather.sunset = sunset;
 	}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@defaultmodules/weather/providers/envcanada.js`:
- Around line 139-151: The switch in `#parseWeatherData` currently falls through
to returning null for unknown this.config.type and that null propagates into
onDataCallback silently; update the default branch to either throw a descriptive
Error or emit a warning (e.g., this.logger.warn or console.warn) that includes
the invalid type, and ensure callers (onDataCallback) never receive null — for
example, have the default return a well-formed error object or throw so upstream
code can handle it; reference `#parseWeatherData`, `#generateCurrentWeather`,
`#generateForecast`, `#generateHourly` and onDataCallback when making the change.

In `@defaultmodules/weather/providers/pirateweather.js`:
- Around line 80-91: The switch on this.config.type in the weather provider
lacks a default branch to handle unknown types; add a default case after the
existing cases that handles unexpected values (e.g., call a fallback, set
weatherData to null/empty, and/or log an error) and include the config type in
the log message; update the switch around generateCurrentWeather,
generateForecast, and generateHourly to ensure unknown this.config.type values
are safely handled and surfaced.
- Around line 17-30: In initialize(), ensure the missing API-key path propagates
errors even when setCallbacks() hasn't been called: when !this.config.apiKey
prepare the error object and if this.onErrorCallback is defined call it,
otherwise throw (or return a rejected Promise) with that error so callers see
the failure; update the same block that currently calls Log.error and
this.onErrorCallback to always either call the callback or throw/reject, keeping
initializeFetcher() untouched.

In `@defaultmodules/weather/providers/smhi.js`:
- Around line 94-105: The switch on this.config.type in the method that sets
weatherData (using `#generateCurrentWeather`, `#generateForecast`, `#generateHourly`)
lacks a default branch, so unknown types leave weatherData undefined and still
get passed to onDataCallback; add a default case that handles unexpected values
(e.g., log an error or call onDataCallback with an error/empty payload) and/or
throw a clear error indicating the invalid config.type so you never call
onDataCallback with undefined weatherData.

In `@defaultmodules/weather/providers/ukmetofficedatahub.js`:
- Around line 38-51: The initialize() method currently skips validating that
required callbacks are set before starting; add a guard that checks
this.onErrorCallback (and any other required callbacks e.g. data/update
callbacks used later) are functions, log an error via Log.error and invoke
this.onErrorCallback with the same error payload used for missing API key, then
return early; only call this.#initializeFetcher() after the callback validation
passes. Reference initialize(), this.onErrorCallback, and `#initializeFetcher`()
when making the change.
- Around line 121-132: Add a default branch to the switch on this.config.type to
handle unknown types so we never pass null into onDataCallback; inside the
default case set weatherData to an empty object (e.g. {}) and log the unexpected
type using the module's logger (e.g. this.logger.error or console.error) with
the offending this.config.type and a short message, then break—this ensures
`#generateCurrentWeather`, `#generateForecast`, `#generateHourly` are unchanged and
onDataCallback always receives a valid object.

In `@defaultmodules/weather/providers/weatherbit.js`:
- Around line 110-121: The switch on this.config.type can leave weatherData null
for unknown types; add a default case in the switch (in the same block that
calls generateCurrentWeather, generateForecast, generateHourly) that logs or
throws a clear error including the invalid this.config.type and the provider
name, or sets weatherData to a safe fallback, e.g. call a validation/error
handler; ensure the default references this.config.type and affects the same
weatherData variable so unknown types are surfaced instead of silently passing
through.
- Around line 30-43: The initialize() method in WeatherbitProvider uses
this.onErrorCallback but doesn't validate callbacks were set by setCallbacks(),
so errors may not propagate; add a guard at the start of initialize() that
verifies required callbacks (at least this.onErrorCallback and any success
callback your provider expects) are present, and if not log an error and return
(or invoke a fallback error path) before proceeding to initializeFetcher();
mirror the validation approach used in OpenWeatherMapProvider (check callbacks
set in initialize(), reference initialize(), setCallbacks(), and
onErrorCallback).

In `@defaultmodules/weather/providers/weatherflow.js`:
- Around line 121-124: In generateCurrentWeather, add defensive null/shape
checks before reading nested API properties: verify data is defined and
data.current_conditions exists before using it (the current variable), and
verify data.forecast && Array.isArray(data.forecast.daily) &&
data.forecast.daily.length > 0 before accessing data.forecast.daily[0] (the
daily variable); on missing values return a safe fallback (null or an object
with default values) or throw a clear error so callers of generateCurrentWeather
handle incomplete API responses.
- Around line 187-212: The generateHourly method assumes data.forecast.hourly
exists; add a defensive check at the start of generateHourly to return an empty
array if data, data.forecast, or data.forecast.hourly is missing or not an array
before iterating; keep the rest of the logic unchanged and reference
generateHourly and data.forecast.hourly so the early-return guard prevents
runtime errors when hourly data is absent.
- Around line 146-180: The generateForecast function lacks null checks and uses
an unsafe date comparison; first guard that data and data.forecast and both
data.forecast.daily and data.forecast.hourly are arrays and return [] if not;
then replace the per-hour matching logic in generateForecast so you compute a
precise time window for each forecast day (const startMs =
forecast.day_start_local * 1000; const endMs = startMs + 24*60*60*1000) and
include hours where const hourMs = hour.time * 1000 and hourMs >= startMs &&
hourMs < endMs (use this to update uvIndex and precipitationAmount), and remove
the current getDate() check and the faulty break based on hourDate >
forecastDate. This uses the unique symbol generateForecast and the
forecast.daily/hourly properties to locate and fix the code.

In `@defaultmodules/weather/providers/weathergov.js`:
- Around line 34-47: The initialize() method of WeathergovProvider doesn't
validate that callbacks were provided before starting async setup; before
calling this.#fetchWeatherGovURLs() and this.#initializeFetcher() add a check
(similar to OpenWeatherMapProvider) to ensure this.onErrorCallback (and any
other required callbacks set by setCallbacks()) are present and if not, invoke
Log.error and throw/propagate an Error (or call the onErrorCallback if present)
so initialization stops and errors are surfaced consistently; modify
initialize() to validate callbacks up-front, reference the existing
setCallbacks(), onErrorCallback, `#fetchWeatherGovURLs`, and `#initializeFetcher`
symbols when implementing the guard.
- Around line 176-196: The switch over this.config.type in the data handling
block (cases "current", "forecast"/"daily", "hourly") lacks a default branch, so
unknown types can result in undefined passed to onDataCallback; add a default
case that either throws a descriptive Error (e.g., `Unknown weather provider
type: ${this.config.type}`) or calls onDataCallback with a clear error,
referencing the switch that populates weatherData and the methods
`#generateWeatherObjectFromCurrentWeather`, `#generateWeatherObjectsFromForecast`,
and `#generateWeatherObjectsFromHourly` so you ensure unknown config.type values
are caught and handled rather than falling through.
🧹 Nitpick comments (17)
defaultmodules/weather/weatherobject.js (1)

64-77: Update JSDoc to reflect nullable return type.

The JSDoc on line 69 states @returns {string} "sunset" or "sunrise", but the function now returns null when sunrise/sunset data is unavailable (lines 72-75). Callers need to be aware of this change.

📝 Proposed documentation update
 	/**
 	 * Determines if the sun sets or rises next. Uses the current time and not
 	 * the date from the weather-forecast.
 	 * `@param` {Moment} date an optional date where you want to get the next
 	 * action for. Useful only in tests, defaults to the current time.
-	 * `@returns` {string} "sunset" or "sunrise"
+	 * `@returns` {string|null} "sunset", "sunrise", or null if sun data unavailable
 	 */
tests/configs/modules/weather/hourlyweather_default.js (1)

15-15: Minor formatting inconsistency.

The closing brace is on the same line as apiKey, unlike other test config files which place it on a separate line. This is a minor consistency issue.

🔧 Suggested formatting fix
-				apiKey: "test-api-key" }
+				apiKey: "test-api-key"
+			}
tests/unit/modules/default/weather/weather_providers_spec.js (1)

10-20: Consider scoping the fetch mock to avoid test pollution.

The global fetch mock is set at module load time and restored in afterAll. If other test files run in the same process before this file's afterAll, they may unexpectedly use this mock. Consider using beforeAll/afterAll within the describe block or vi.spyOn for tighter scoping.

♻️ Proposed refactor using beforeAll/afterAll
-// Mock global fetch for location lookup
-const originalFetch = global.fetch;
-
-global.fetch = vi.fn(() => Promise.resolve({
-	ok: true,
-	json: () => Promise.resolve({ city: "Munich", locality: "Munich" })
-}));
-
-// Restore original fetch after all tests
-afterAll(() => {
-	global.fetch = originalFetch;
-});
-
 describe("Weather Provider Smoke Tests", () => {
+	let originalFetch;
+
+	beforeAll(() => {
+		originalFetch = global.fetch;
+		global.fetch = vi.fn(() => Promise.resolve({
+			ok: true,
+			json: () => Promise.resolve({ city: "Munich", locality: "Munich" })
+		}));
+	});
+
+	afterAll(() => {
+		global.fetch = originalFetch;
+	});
+
 	describe("OpenMeteoProvider", () => {
tests/electron/helpers/weather-setup.js (2)

11-73: Consider extracting shared mock data transformation logic.

The injectMockWeatherData function duplicates the transformation logic found in test/e2e/helpers/weather-functions.js (lines 10-56). Both files transform raw mock data identically for current, forecast, and hourly types.

Consider extracting the shared transformation logic into a common utility to reduce maintenance burden and ensure consistency.


88-99: Hardcoded timeouts may cause flaky tests.

Using fixed waitForTimeout(1000) and waitForTimeout(500) can lead to flaky tests depending on system load. Consider using condition-based waiting instead.

♻️ Suggested improvement
 exports.startApp = async (configFileName, systemDate, mockDataFile = "weather_onecall_current.json") => {
 	await helpers.startApplication(configFileName, systemDate);

-	// Wait for modules to initialize
-	await global.page.waitForTimeout(1000);
+	// Wait for weather module to be ready
+	await global.page.waitForFunction(() => {
+		const weatherModule = MM?.getModules()?.find((m) => m.name === "weather");
+		return weatherModule && weatherModule.instanceId;
+	}, { timeout: 5000 });

 	// Inject mock weather data
 	await injectMockWeatherData(mockDataFile);

-	// Wait for rendering
-	await global.page.waitForTimeout(500);
+	// Wait for weather content to render
+	await global.page.waitForSelector(".weather .normal.medium", { timeout: 5000 });
 };
defaultmodules/weather/providers/pirateweather.js (1)

4-10: Constructor doesn't merge default configuration values.

Unlike other providers (e.g., WeatherbitProvider, OpenWeatherMapProvider) that merge defaults with the provided config, this constructor assigns config directly. This means missing config properties won't have fallback values.

♻️ Suggested fix for consistency
 class PirateweatherProvider {
 	constructor (config) {
-		this.config = config;
+		this.config = {
+			apiBase: "https://api.pirateweather.net",
+			weatherEndpoint: "/forecast",
+			apiKey: "",
+			lat: 0,
+			lon: 0,
+			type: "current",
+			lang: "en",
+			updateInterval: 10 * 60 * 1000,
+			...config
+		};
 		this.fetcher = null;
defaultmodules/weather/provider-utils.js (3)

70-77: Consider hoisting require("suncalc") to module level.

Requiring suncalc inside the function on every call adds overhead. Since this is a server-side module, hoisting the require to module level improves performance without any downsides.

♻️ Suggested improvement
+const SunCalc = require("suncalc");
+
 /**
  * Shared utility functions for weather providers
  */
 
 // ... other functions ...

 function getSunTimes (date, lat, lon) {
-	const SunCalc = require("suncalc");
 	const sunTimes = SunCalc.getTimes(date, lat, lon);

132-140: validateCoordinates mutates the input config object.

This function has a hidden side effect - it modifies config.lat and config.lon in place. This could surprise callers who don't expect their config to be modified. Consider either documenting this clearly or returning new values instead.

♻️ Alternative: return validated values without mutation
-function validateCoordinates (config, maxDecimals = 4) {
+function validateCoordinates (config, maxDecimals = 4) {
 	if (config.lat == null || config.lon == null
 		|| !Number.isFinite(config.lat) || !Number.isFinite(config.lon)) {
 		throw new Error("Latitude and longitude are required");
 	}

-	config.lat = limitDecimals(config.lat, maxDecimals);
-	config.lon = limitDecimals(config.lon, maxDecimals);
+	return {
+		lat: limitDecimals(config.lat, maxDecimals),
+		lon: limitDecimals(config.lon, maxDecimals)
+	};
 }

52-61: limitDecimals may not handle scientific notation correctly.

For very small or large numbers that JavaScript represents in scientific notation (e.g., 1e-10), the string-based approach won't find a decimal point where expected.

🛡️ More robust alternative
 function limitDecimals (value, decimals) {
+	// Handle edge cases
+	if (!Number.isFinite(value)) return value;
+	
 	const str = value.toString();
+	// Handle scientific notation
+	if (str.includes("e") || str.includes("E")) {
+		const factor = Math.pow(10, decimals);
+		return Math.trunc(value * factor) / factor;
+	}
 	if (str.includes(".")) {
defaultmodules/weather/providers/yr.js (1)

83-104: Stellar data fetch lacks timeout protection.

The fetch call for stellar data has no timeout, which could cause initialize() to hang indefinitely if the Yr.no sunrise API is unresponsive. Consider adding an AbortController with a reasonable timeout.

🛡️ Proposed fix
 	async `#fetchStellarData` () {
 		const today = getDateString(new Date());
 
 		// Check if we already have today's data
 		if (this.stellarDataDate === today && this.stellarData) {
 			return;
 		}
 
 		const url = this.#getSunriseUrl();
+		const controller = new AbortController();
+		const timeoutId = setTimeout(() => controller.abort(), 10000);
 
 		try {
 			const response = await fetch(url, {
 				headers: {
 					"User-Agent": "MagicMirror",
 					Accept: "application/json"
-				}
+				},
+				signal: controller.signal
 			});
+			clearTimeout(timeoutId);
defaultmodules/weather/weather.js (3)

98-107: Instance ID generation uses Date.now() which may collide in rapid initialization.

If multiple weather modules are initialized in the same millisecond (unlikely but possible during rapid page loads), Date.now() could produce duplicate IDs. Consider adding a counter or random component.

🔧 Proposed fix
-		this.instanceId = `${this.identifier}_${Date.now()}`;
+		this.instanceId = `${this.identifier}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;

144-151: CURRENT_WEATHER_OVERRIDE silently no-ops if currentWeatherObject is null.

If an override notification arrives before the first weather data is received, it will be silently ignored. Consider logging a warning or queuing the override.


171-193: Missing default case in handleWeatherData switch.

If type is not one of the expected values, the function silently returns without error. Consider adding a warning log for unexpected types.

🛡️ Proposed fix
 			case "hourly":
 				this.weatherHourlyArray = data.map((d) => this.createWeatherObject(d));
 				break;
+			default:
+				Log.warn(`[weather] Unknown weather data type: ${type}`);
+				return;
 		}
defaultmodules/weather/providers/smhi.js (1)

296-302: Coordinate resolution assumes specific array structure.

The code assumes data.geometry.coordinates[0] is an array with [lon, lat] format. If the API response structure changes or is malformed, this could throw. Consider adding defensive checks.

🛡️ Proposed fix
 	`#resolveCoordinates` (data) {
 		// SMHI returns coordinates in [lon, lat] format
+		if (!data?.geometry?.coordinates?.[0]?.[0] || !data?.geometry?.coordinates?.[0]?.[1]) {
+			Log.warn("[weatherprovider.smhi] Invalid coordinate structure, using config values");
+			return { lat: this.config.lat, lon: this.config.lon };
+		}
 		return {
 			lat: data.geometry.coordinates[0][1],
 			lon: data.geometry.coordinates[0][0]
 		};
 	}
defaultmodules/weather/providers/openmeteo.js (2)

145-160: Geocoding fetch lacks timeout and error handling for non-JSON responses.

The #fetchLocation method doesn't handle network timeouts or non-JSON error responses from BigDataCloud. A malformed response would cause response.json() to throw.

🛡️ Proposed fix
 	async `#fetchLocation` () {
 		const url = `${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang || "en"}`;
+		const controller = new AbortController();
+		const timeoutId = setTimeout(() => controller.abort(), 5000);
 
 		try {
-			const response = await fetch(url);
+			const response = await fetch(url, { signal: controller.signal });
+			clearTimeout(timeoutId);
 			if (!response.ok) {
 				throw new Error(`HTTP ${response.status}`);
 			}

518-519: Hourly weather filtering logic may skip valid entries.

The condition hours.length === 0 && weather.time <= now will skip all entries until the first future entry is found, which is correct. However, using <= means an entry exactly at the current time is also skipped. Consider using < if you want to include the current hour.

defaultmodules/weather/providers/envcanada.js (1)

319-327: Multiple regex patterns used for XML parsing are fragile to schema changes.

The provider extensively uses regex to extract data from Environment Canada XML responses (forecast blocks, temperature, precipitation, icon codes, hourly forecasts, etc.). While functional, this approach is prone to breaking with whitespace changes, attribute reordering, or XML structure modifications. Consider adding an XML parser library (e.g., fast-xml-parser) to handle these edge cases more robustly if the API schema proves unstable.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@defaultmodules/weather/providers/openmeteo.js`:
- Around line 474-495: The code assumes parsedData.daily is an object of arrays
but after `#transposeDataMatrix` parsedData.daily is an array of daily objects;
update the accesses to read the first day's properties (e.g.
parsedData.daily[0].sunrise, parsedData.daily[0].sunset,
parsedData.daily[0].temperature_2m_min, parsedData.daily[0].temperature_2m_max)
when populating
current.sunrise/current.sunset/current.minTemperature/current.maxTemperature,
and then call this.#convertWeatherType(parsedData.current_weather.weathercode,
this.#isDayTime(parsedData.current_weather.time, current.sunrise,
current.sunset)) once sunrise and sunset are set.

In `@defaultmodules/weather/providers/openweathermap.js`:
- Around line 182-183: The code currently uses a truthy check that turns 0 into
undefined (e.g., weather.precipitationProbability = hour.pop ? hour.pop * 100 :
undefined); change this to a nullish or explicit undefined check so 0% is
preserved — e.g. set weather.precipitationProbability = (hour.pop ?? undefined)
!== undefined ? hour.pop * 100 : undefined or equivalently
weather.precipitationProbability = hour.pop !== undefined ? hour.pop * 100 :
undefined; make the same change for the other occurrence that assigns
precipitationProbability from day.pop (replace the truthy check with a
nullish/explicit undefined check).

In `@defaultmodules/weather/providers/pirateweather.js`:
- Around line 227-231: getUrl currently hardcodes "units=si" and can emit
"lang=undefined"; update it to use the module config defaults instead: read
units via this.config.units (fallback to "us") and lang via this.config.lang
(fallback to "en") when building the query string, and also add default entries
for units and lang in the module's defaults object (the defaults defined near
the top of the file) or ensure the getUrl fallbacks are used; specifically
modify getUrl to compute const units = this.config.units || "us" and const lang
= this.config.lang || "en" and replace the hardcoded units=si and lang usages
with those variables so URLs reflect the config.

In `@defaultmodules/weather/providers/smhi.js`:
- Around line 104-118: The switch's default branch logs an error for unknown
this.config.type but then allows onDataCallback(weatherData) to be called with
weatherData === undefined; change the default branch in the switch so it does
not fall through: either return early after Log.error(`[smhi] Unknown weather
type: ${this.config.type}`) or set weatherData to a valid error/result object
and call onDataCallback with an explicit error payload; ensure onDataCallback is
only invoked when weatherData is defined (refer to weatherData, onDataCallback,
this.config.type and Log.error).

In `@defaultmodules/weather/providers/weatherbit.js`:
- Around line 167-197: The precipitation field names in generateForecast and
generateHourly are incorrect: change the forecast/hourly output objects to use
precipitationAmount instead of precipitation so they match the WeatherObject
schema and templates; update both generateForecast (the object pushed in the
loop for daily entries) and generateHourly (the object pushed in the loop for
hourly entries) to replace the precipitation property with precipitationAmount
while keeping the same parsed value (parseFloat(forecast.precip) or default 0).

In `@defaultmodules/weather/providers/yr.js`:
- Around line 73-104: The `#fetchStellarData` method currently never parses or
saves the fetched JSON, so this.stellarData remains unset; fix it by, after
confirming response.ok, calling await response.json(), assigning the parsed
object to this.stellarData and setting this.stellarDataDate = today (and
optionally return the data); ensure this happens inside the try block (before
clearTimeout or immediately after) and preserve the existing error/timeout
handling and Log.warn behavior in the non-ok and catch branches so
`#getStellarInfoForDate` can read the stored stellar data.

In `@tests/electron/helpers/weather-setup.js`:
- Around line 19-56: The helper in tests/electron/helpers/weather-setup.js
currently leaves data null when rawData lacks current/daily/hourly; add an
explicit validation guard after reading rawData (e.g., check rawData.current ||
rawData.daily || rawData.hourly) and if none are present throw a clear Error (or
return a rejected/invalid marker) so invalid fixtures fail fast; update the
logic around the variables type and data to only proceed when that validation
passes (referencing rawData, type, data and the mapping branches for
current/daily/hourly).
🧹 Nitpick comments (5)
tests/electron/helpers/weather-setup.js (1)

91-98: Fixed timeouts may cause flaky tests.

The hardcoded waitForTimeout(1000) and waitForTimeout(500) are less reliable than selector-based waits. Consider using waitForSelector similar to the E2E helper, which waits for .weather and .weather .weathericon elements with explicit timeouts.

♻️ Suggested improvement
 exports.startApp = async (configFileName, systemDate, mockDataFile = "weather_onecall_current.json") => {
 	await helpers.startApplication(configFileName, systemDate);
 
-	// Wait for modules to initialize
-	await global.page.waitForTimeout(1000);
+	// Wait for weather module to initialize
+	await global.page.waitForSelector(".weather", { timeout: 5000 });
 
 	// Inject mock weather data
 	await injectMockWeatherData(mockDataFile);
 
-	// Wait for rendering
-	await global.page.waitForTimeout(500);
+	// Wait for data to be rendered
+	await global.page.waitForSelector(".weather .weathericon", { timeout: 2000 });
 };
tests/configs/modules/weather/hourlyweather_default.js (1)

15-15: Minor formatting inconsistency.

The closing brace is on the same line as apiKey, unlike other weather config files where each property has its own line. Consider moving it to a separate line for consistency.

🔧 Suggested fix
-				apiKey: "test-api-key" }
+				apiKey: "test-api-key"
+			}
defaultmodules/weather/providers/pirateweather.js (1)

101-104: Default case logs error but doesn't notify via callback.

The default case now exists (addressing the past review comment), but unlike other error paths in this file, it doesn't invoke onErrorCallback to notify the caller.

🛡️ Proposed fix to invoke error callback
 			default:
 				Log.error(`[pirateweather] Unknown weather type: ${this.config.type}`);
+				if (this.onErrorCallback) {
+					this.onErrorCallback({
+						message: `Unknown weather type: ${this.config.type}`,
+						translationKey: "MODULE_ERROR_UNSPECIFIED"
+					});
+				}
 				break;
defaultmodules/weather/weather.js (1)

166-168: Consider displaying errors to the user.

When WEATHER_ERROR is received, the error is only logged. The user won't see any indication that weather data failed to load. Consider updating the DOM with an error state or using this.updateDom() to show a loading/error message.

defaultmodules/weather/providers/openmeteo.js (1)

436-443: Time comparison using String() is fragile.

Both hour.time and currentTime are Date objects. Using String() comparison relies on identical string representations, which may vary. A more robust approach would compare timestamps directly.

🔧 Proposed fix
 			if (Array.isArray(parsedData.hourly)) {
 				// Array of objects (after transpose)
-				const hourlyIndex = parsedData.hourly.findIndex((hour) => String(hour.time) === String(currentTime));
+				const hourlyIndex = parsedData.hourly.findIndex((hour) => hour.time.getTime() === currentTime.getTime());
 				h = hourlyIndex !== -1 ? hourlyIndex : 0;

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@defaultmodules/weather/providers/weatherflow.js`:
- Line 226: The hourly windSpeed assignment uses convertKmhToMs(hour.wind_avg)
without null-checking hour.wind_avg; update the expression in the hourly mapping
(where windSpeed is set) to guard against missing values by checking
hour.wind_avg for null/undefined and only calling convertKmhToMs when present,
otherwise return null/undefined (match the same null-check pattern used
elsewhere in this module).
- Line 150: The windSpeed assignment can produce NaN when current.wind_avg is
undefined; change the expression to only call convertKmhToMs when
current.wind_avg is present and otherwise return null (e.g., replace
convertKmhToMs(current.wind_avg) with a conditional like current.wind_avg !=
null ? convertKmhToMs(current.wind_avg) : null) so that convertKmhToMs is not
called with undefined and windSpeed follows the same fallback pattern as other
fields.
🧹 Nitpick comments (8)
tests/configs/modules/weather/hourlyweather_default.js (1)

10-16: Minor formatting inconsistency.

The closing brace is on the same line as apiKey. For consistency with forecastweather_default.js, consider placing it on a separate line.

🔧 Suggested formatting fix
 			config: {
 				lat: 48.14,
 				lon: 11.58,
 				type: "hourly",
 				weatherProvider: "openweathermap",
-				apiKey: "test-api-key" }
+				apiKey: "test-api-key"
+			}
 		}
js/http_fetcher.js (1)

272-275: Consider wrapping URL parsing in try-catch for robustness.

While the URL is likely valid if fetch() was attempted, malformed URLs could cause new URL() to throw, potentially masking the original network error. Consider a defensive wrapper.

🛡️ Suggested defensive improvement
 			// Truncate URL for cleaner logs
-			const urlObj = new URL(this.url);
-			const shortUrl = `${urlObj.origin}${urlObj.pathname}${urlObj.search.length > 50 ? "?..." : urlObj.search}`;
+			let shortUrl = this.url;
+			try {
+				const urlObj = new URL(this.url);
+				shortUrl = `${urlObj.origin}${urlObj.pathname}${urlObj.search.length > 50 ? "?..." : urlObj.search}`;
+			} catch {
+				// Use original URL if parsing fails
+			}
 			Log.error(`${this.logContext}${shortUrl} - ${message}`);
defaultmodules/weather/provider-utils.js (4)

35-35: Consider using direct property access instead of hasOwnProperty.

Using weatherTypes[weatherType] ?? null or Object.hasOwn(weatherTypes, weatherType) is more concise and equally safe for object literals.

Suggested simplification
-	return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
+	return weatherTypes[weatherType] ?? null;

55-64: Edge case: scientific notation not handled.

Values like 1.23e-10 would pass the includes(".") check but the string split would produce unexpected results. For coordinate truncation, this is unlikely but worth noting.

More robust alternative using Math
 function limitDecimals (value, decimals) {
-	const str = value.toString();
-	if (str.includes(".")) {
-		const parts = str.split(".");
-		if (parts[1].length > decimals) {
-			return parseFloat(`${parts[0]}.${parts[1].substring(0, decimals)}`);
-		}
-	}
-	return value;
+	if (!Number.isFinite(value)) return value;
+	const factor = Math.pow(10, decimals);
+	return Math.trunc(value * factor) / factor;
 }

73-79: Potential issue with polar regions.

SunCalc returns Invalid Date for locations where the sun doesn't rise or set (polar day/night). Callers like isDayTime handle null but not Invalid Date. Consider adding validation.

Suggested defensive check
 function getSunTimes (date, lat, lon) {
 	const sunTimes = SunCalc.getTimes(date, lat, lon);
+	const sunrise = isNaN(sunTimes.sunrise?.getTime()) ? null : sunTimes.sunrise;
+	const sunset = isNaN(sunTimes.sunset?.getTime()) ? null : sunTimes.sunset;
 	return {
-		sunrise: sunTimes.sunrise,
-		sunset: sunTimes.sunset
+		sunrise,
+		sunset
 	};
 }

134-142: Function mutates input config, which may be unexpected.

The function name suggests validation only, but it also truncates config.lat and config.lon in-place. Consider either returning the modified values or renaming to validateAndTruncateCoordinates to make the side effect explicit.

defaultmodules/weather/providers/ukmetofficedatahub.js (1)

132-139: Default case only logs; consider calling error callback.

The default case logs the unknown type but doesn't call onErrorCallback. While the null check at line 137 prevents passing null to onDataCallback, the caller won't know why no data arrived.

Suggested improvement
 		default:
 			Log.error(`[ukmetofficedatahub] Unknown weather type: ${this.config.type}`);
+			if (this.onErrorCallback) {
+				this.onErrorCallback({
+					message: `Unknown weather type: ${this.config.type}`,
+					translationKey: "MODULE_ERROR_UNSPECIFIED"
+				});
+			}
+			return;
-			break;
 		}
defaultmodules/weather/providers/envcanada.js (1)

71-81: Consider potential infinite loop on persistent hour change.

If #initializeFetcher is called and the hour changes during fetch processing, this could cause repeated reinitialization. This is likely rare in practice, but adding a guard (e.g., a flag to prevent re-entry during reinitialization) could improve robustness.

♻️ Optional: Add re-entry guard
 	`#initializeFetcher` () {
+		if (this.reinitializing) return;
 		this.currentHour = new Date().toISOString().substring(11, 13);
 		const indexURL = this.#getIndexUrl();
 
@@ -73,6 +74,7 @@
 				const newHour = new Date().toISOString().substring(11, 13);
 				if (newHour !== this.currentHour) {
 					Log.info("[envcanada] Hour changed, reinitializing fetcher");
+					this.reinitializing = true;
 					this.stop();
 					this.#initializeFetcher();
 					this.start();
+					this.reinitializing = false;
 					return;
 				}

@KristjanESPERANTO KristjanESPERANTO changed the base branch from develop to master February 7, 2026 18:26
@KristjanESPERANTO KristjanESPERANTO changed the base branch from master to develop February 7, 2026 18:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@defaultmodules/weather/providers/openmeteo.js`:
- Line 529: The computed daily index h = Math.ceil((i + 1) / 24) - 1 can be out
of range for parsedData.daily; add a defensive bounds check before using
parsedData.daily[h] in the hourly loop (the block around the h calculation and
the accesses at lines ~534-541). Specifically, ensure parsedData.daily exists
and use a safe index (e.g., if h < 0 set h = 0, if h >= parsedData.daily.length
set h = parsedData.daily.length - 1) or skip setting day-specific fields when no
matching daily entry exists, then read sunrise/sunset/other fields from
parsedData.daily[safeH] instead of parsedData.daily[h]. Ensure any early-return
or default fallback prevents accessing properties on undefined.
- Around line 524-527: The hourly filtering in parsedData.hourly.forEach
currently uses (hours.length === 0 && weather.time <= now) which incorrectly
skips the first valid future entry; change the logic in that loop so you skip
any entry where weather.time <= now (i.e., only process entries with
weather.time > now) and push entries until hours.length reaches
this.config.maxEntries, ensuring the first future hour is included; update the
condition that references parsedData.hourly, hours, now, and
this.config.maxEntries accordingly.

In `@defaultmodules/weather/providers/pirateweather.js`:
- Around line 124-167: The code incorrectly treats numeric zeros as missing by
using truthy checks (e.g., data.currently.humidity ? ...,
data.currently.windSpeed ? ..., forecast.precipProbability ? ...), so update
generateCurrent (the current object construction) and generateForecast to check
for undefined/null instead of falsiness: use explicit checks like
data.currently.humidity !== undefined && data.currently.humidity !== null (or
Object.prototype.hasOwnProperty) before parsing/multiplying, and similarly for
windSpeed, apparentTemperature, forecast.precipProbability, temperatureMin/Max
etc., so 0 values are preserved as 0 rather than converted to null; keep
conversions (parseFloat, *100) the same when the value is present.

In `@js/http_fetcher.js`:
- Around line 272-275: The current logging creates a URL object from this.url
(new URL(this.url)) which can throw and hide the original error; wrap the URL
parsing in a try/catch inside the same block that builds shortUrl (used by
Log.error) and on failure fall back to a safe sanitized representation (e.g.,
the raw this.url or a trimmed "invalid-url" string), then call
Log.error(`${this.logContext}${shortUrl} - ${message}`); ensure the change is
applied around the existing shortUrl construction so references to this.url,
shortUrl and Log.error remain and no exceptions are thrown while logging.

In `@js/server_functions.js`:
- Line 94: Remove the dead getVersion function and its export: delete the
getVersion function definition from server_functions.js and update the
module.exports object (the exported names list that currently includes
getVersion) to no longer reference getVersion so only existing used functions
(getHtml, getEnvVars, getEnvVarsAsObj, getUserAgent, getConfigFilePath) are
exported. Ensure there are no remaining references to getVersion after this
change.

In `@tests/e2e/helpers/weather-functions.js`:
- Around line 54-65: The precipitationProbability calculation in the hourly
branch treats 0 as falsy and drops valid 0% values; update the ternary in the
hourly map (inside tests/e2e/helpers/weather-functions.js where rawData.hourly
is mapped) to use a nullish check (or explicit null/undefined check) against
hour.pop so that 0 is preserved (e.g., check hour.pop != null or hour.pop ??
...) and only produce undefined when pop is null/undefined.

In `@tests/electron/helpers/weather-setup.js`:
- Around line 51-54: The precipitationProbability check uses a truthy test that
drops 0 (hour.pop) causing 0% to become undefined; update the conditional in
weather-setup.js for precipitationProbability to use a nullish/explicit null
check (e.g., check hour.pop != null) so that hour.pop == 0 is preserved and you
multiply by 100 only when hour.pop is not null/undefined; locate the
precipitationProbability assignment in the diff and replace the truthy ternary
with a null-checking ternary or nullish-aware logic.

In `@tests/unit/modules/default/weather/providers/envcanada_spec.js`:
- Around line 143-146: The comment above the assertion for result.feelsLikeTemp
is incorrect: update the comment that currently reads "XML has windChill of -12"
to match the XML mock value of -31 so it correctly documents the expectation for
the test that checks result.feelsLikeTemp equals -31; locate the test/assertion
referencing result.feelsLikeTemp in the envcanada_spec.js block and change only
the comment text to "XML has windChill of -31".
- Around line 286-307: The callbacks are wired incorrectly in the test: when
calling provider.setCallbacks(...) the first callback should set a data flag and
the second should set an error flag, but the current test sets errorCalled
inside the data callback and resolves errorPromise inside the error callback,
causing inverted logic. Fix by passing two named inline callbacks to
setCallbacks where the first sets a dataCalled boolean (or noop) and the second
sets errorCalled = true and resolves errorPromise; then either await
errorPromise with a timeout to assert it is not resolved (use Promise.race with
a short timeout) or simply wait a short delay and assert errorCalled is false
after calling provider.initialize() and provider.start(); ensure you reference
EnvCanadaProvider, setCallbacks, initialize, and start when locating the test
code to update.

In `@tests/unit/modules/default/weather/providers/openmeteo_spec.js`:
- Around line 93-104: The test contains debug console.log statements inside the
Promise that wraps provider.setCallbacks (the two callbacks passed to
provider.setCallbacks and the variable dataPromise); remove the two console.log
calls ("[TEST] onDataCallback called" and "[TEST] onErrorCallback called:") so
the callbacks only call resolve(data) and reject(error) without printing to
stdout.
🧹 Nitpick comments (5)
tests/mocks/weather_ukmetoffice.json (1)

1020-1035: Last timeSeries entry is missing fields present in other entries.

The final hourly entry (2026-02-09T12:00Z) is missing several fields that are present in all other entries: maxScreenAirTemp, minScreenAirTemp, totalPrecipAmount, and totalSnowAmount.

If this is intentional to test the provider's handling of incomplete data from the API, consider adding a brief comment in a companion test file documenting this. Otherwise, add the missing fields for consistency.

js/server_functions.js (1)

5-5: Dead code: startUp variable is no longer used.

The startUp variable was previously used by the removed getStartup function. It can be safely deleted.

🧹 Proposed fix to remove dead code
 const Log = require("logger");
 
-const startUp = new Date();
-
 /**
  * Gets the HTML to display the magic mirror.
tests/unit/modules/default/weather/providers/openweathermap_spec.js (1)

88-101: Consider adding timeout protection for callback-based tests.

The dataPromise pattern is clean, but if the callback is never invoked (e.g., due to a bug in the provider), the test will hang indefinitely rather than failing with a clear message.

💡 Optional: Add timeout wrapper
-			const dataPromise = new Promise((resolve) => {
-				provider.setCallbacks(resolve, vi.fn());
-			});
+			const dataPromise = Promise.race([
+				new Promise((resolve) => {
+					provider.setCallbacks(resolve, vi.fn());
+				}),
+				new Promise((_, reject) =>
+					setTimeout(() => reject(new Error("Test timeout")), 5000)
+				)
+			]);

Alternatively, Vitest's built-in test timeout should handle this, but an explicit timeout provides clearer failure messages.

tests/unit/modules/default/weather/providers/smhi_spec.js (1)

13-13: Unused constant SMHI_URL.

This constant is declared but never used in the tests. Consider removing it or using it in the HTTP mock setup for consistency.

🧹 Proposed fix
-const SMHI_URL = "https://opendata-download-metfcst.smhi.se/*";

Or use it in the http.get() calls instead of the inline string.

defaultmodules/weather/providers/weathergov.js (1)

341-342: Both min and max temperature arrays receive the same value.

Both minTemp and maxTemp arrays push forecast.temperature, which is the same value. While the intent is to track temperature values across a day and then compute min/max, this works correctly. However, it would be clearer to use a single array:

♻️ Optional simplification
 		let minTemp = [];
-		let maxTemp = [];
+		let temps = [];
 		let date = "";
 		let weather = {};

 		for (const forecast of forecasts) {
 			// ...
 			if (date !== dateStr) {
 				if (date !== "") {
-					weather.minTemperature = Math.min(...minTemp);
-					weather.maxTemperature = Math.max(...maxTemp);
+					weather.minTemperature = Math.min(...temps);
+					weather.maxTemperature = Math.max(...temps);
 					days.push(weather);
 				}

 				weather = {};
-				minTemp = [];
-				maxTemp = [];
+				temps = [];
 				// ...
 			}
 			// ...
-			minTemp.push(forecast.temperature);
-			maxTemp.push(forecast.temperature);
+			temps.push(forecast.temperature);
 		}

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Migrates the Weather module to server-side provider execution coordinated by node_helper, leveraging centralized HTTPFetcher behavior and adding comprehensive provider unit tests with MSW fixtures.

Changes:

  • Replaced client-side weather provider fetching (and CORS proxy dependency) with server-side providers wired via node_helper.
  • Enhanced HTTPFetcher logging with logContext and improved error messaging/URL truncation.
  • Added/updated extensive unit and E2E/Electron test fixtures and helper utilities for injecting weather data via sockets.

Reviewed changes

Copilot reviewed 68 out of 78 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/unit/modules/default/weather/providers/smhi_spec.js Adds SMHI provider unit coverage using MSW.
tests/unit/modules/default/weather/providers/pirateweather_spec.js Adds Pirate Weather provider unit coverage using MSW.
tests/unit/modules/default/weather/providers/openweathermap_spec.js Adds OpenWeatherMap provider unit coverage using MSW.
tests/unit/modules/default/weather/providers/openmeteo_spec.js Adds OpenMeteo provider unit coverage including geocoding mocking.
tests/unit/modules/default/weather/providers/envcanada_spec.js Adds EnvCanada provider unit coverage for the two-step fetch and XML parsing.
tests/unit/modules/default/weather/provider_utils_spec.js Adds tests for new shared provider utility functions.
tests/unit/modules/default/utils_spec.js Removes tests for deleted performWebRequest (CORS path).
tests/unit/functions/server_functions_spec.js Removes CORS tests; keeps getUserAgent coverage and restores global config handling.
tests/mocks/weather_weathergov_points.json Adds WeatherGov points fixture.
tests/mocks/weather_weathergov_forecast.json Adds WeatherGov forecast fixture.
tests/mocks/weather_weathergov_current.json Adds WeatherGov current conditions fixture.
tests/mocks/weather_weatherbit_hourly.json Adds Weatherbit “no access” hourly error fixture.
tests/mocks/weather_weatherbit_forecast.json Adds Weatherbit forecast fixture.
tests/mocks/weather_weatherbit.json Adds Weatherbit current fixture.
tests/mocks/weather_openmeteo_current_weather.json Adds OpenMeteo combined current+forecast fixture.
tests/mocks/weather_onecall_hourly.json Updates OneCall hourly fixture (timezone metadata + timestamps).
tests/mocks/weather_onecall_forecast.json Adds OneCall forecast fixture.
tests/mocks/weather_onecall_current.json Adds OneCall current fixture.
tests/electron/modules/weather_spec.js Updates Electron weather tests to use socket-injected fixtures (no weather_mocker).
tests/electron/helpers/weather-setup.js Adds socket-based mock injector for Electron weather rendering tests.
tests/e2e/modules/weather_hourly_spec.js Updates E2E tests to inject hourly fixture via helper.
tests/e2e/modules/weather_forecast_spec.js Updates E2E tests to inject forecast fixture via helper.
tests/e2e/modules/weather_current_spec.js Updates E2E tests to inject current fixture via helper.
tests/e2e/helpers/weather-functions.js Adds socket-based weather fixture injection in Playwright helper.
tests/configs/modules/weather/hourlyweather_showPrecipitation.js Updates weather test configs to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/hourlyweather_options.js Updates weather test configs to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/hourlyweather_default.js Updates weather test configs to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/forecastweather_units.js Updates forecast config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/forecastweather_options.js Updates forecast config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/forecastweather_default.js Updates forecast config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/forecastweather_absolute.js Updates forecast config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/currentweather_units.js Updates current config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/currentweather_options.js Updates current config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/currentweather_default.js Updates current config to lat/lon + apiKey (no mockData).
tests/configs/modules/weather/currentweather_compliments.js Updates current config to lat/lon + apiKey (no mockData).
js/server_functions.js Removes CORS proxy endpoints/helpers and related exports.
js/server.js Removes /cors and /version routes; keeps /startup via a local closure.
js/http_fetcher.js Adds logContext and improves error logging and URL display.
eslint.config.mjs Relaxes eqeqeq for null checks and disables import rules for new weather provider unit tests.
defaultmodules/weather/weatherutils.js Guards precipitation unit conversion against null/undefined/NaN.
defaultmodules/weather/weatherobject.js Makes sun-based calculations resilient when sunrise/sunset are unavailable.
defaultmodules/weather/weather.js Migrates to server-side coordination via node_helper, removes client-side provider loading/scheduling.
defaultmodules/weather/providers/weatherflow.js Refactors WeatherFlow provider to server-side HTTPFetcher pattern.
defaultmodules/weather/providers/weatherbit.js Refactors Weatherbit provider to server-side HTTPFetcher pattern.
defaultmodules/weather/providers/ukmetofficedatahub.js Refactors UK Met Office provider to server-side HTTPFetcher pattern.
defaultmodules/weather/providers/pirateweather.js Refactors Pirate Weather provider to server-side HTTPFetcher pattern.
defaultmodules/weather/providers/README.md Updates provider dev documentation link.
defaultmodules/weather/provider-utils.js Adds shared provider utilities (timezone, sun times, coordinate validation, etc.).
defaultmodules/weather/node_helper.js Adds server-side provider lifecycle and socket communication for weather.
defaultmodules/weather/current.njk Avoids rendering sun icon when sun data is unavailable.
defaultmodules/utils.js Removes CORS-related performWebRequest, leaving formatTime.
defaultmodules/weather/weatherprovider.js Removes legacy client-side WeatherProvider registry/loader.
defaultmodules/weather/providers/overrideWrapper.js Removes legacy override wrapper (override logic moved into module).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 69 out of 78 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 69 out of 78 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@KristjanESPERANTO KristjanESPERANTO force-pushed the weather branch 2 times, most recently from 2e18b98 to 310a5ac Compare February 8, 2026 11:23
…PFetcher

Migrate the OpenMeteo weather provider from client-side (performWebRequest +
CORS proxy) to server-side architecture using HTTPFetcher for consistency
with Calendar and Newsfeed modules.

Changes:
- Add node_helper.js for server-side weather provider management
- Add openmeteo_server.js using HTTPFetcher with periodic auto-fetch
- Modify weather.js with hybrid client/server-side provider support
- Remove client-side openmeteo.js (now obsolete)

Architecture:
- weather.js sends INIT_WEATHER → node_helper loads provider
- Provider uses HTTPFetcher for automatic polling (reloadInterval)
- Data flows via callbacks → socket notifications → weather.js

Benefits:
- No CORS proxy needed
- API keys stay server-side
- Unified architecture with Calendar/Newsfeed
- HTTPFetcher retry strategies (429/5xx) built-in
Add server-side OpenWeatherMap provider using HTTPFetcher, following the
same pattern as OpenMeteo migration.

Changes:
- Add openweathermap_server.js with OnceCall API 3.0 support
- Update weather.js to recognize openweathermap as server-side provider
- Remove client-side openweathermap.js (now obsolete)

Features:
- Supports current, forecast/daily, and hourly weather types
- Timezone offset handling for accurate timestamps
- UV index, precipitation, and feels-like temperature
- HTTPFetcher automatic polling and retry strategies
Add server-side WeatherGov (US National Weather Service) provider using
HTTPFetcher.

Changes:
- Add weathergov_server.js with 2-step initialization (grid-point → station)
- Update weather.js to recognize weathergov as server-side provider
- Remove client-side weathergov.js (now obsolete)

Implementation notes:
- SunCalc integration for sunrise/sunset (API doesn't provide this)
- User-Agent header required by API
- US locations only, no API key required
Add server-side Yr.no (Norwegian Meteorological Institute) provider
with proper HTTP caching support.

Changes:
- Add yr_server.js using HTTPFetcher for periodic weather fetching
- Implement If-Modified-Since header support per API recommendations
- Handle 304 Not Modified responses to reduce API calls
- Cache Last-Modified and Expires headers for efficient updates
- Fetch stellar data (sunrise/sunset) separately with daily caching
- Update weather.js to recognize yr as server-side provider
- Remove client-side yr.js (now obsolete)

Implementation notes:
- Enforce 10-minute minimum update interval per API terms
- Coordinate precision limited to 4 decimals per API requirements
- Weather data cached in-memory with Last-Modified tracking
- Stellar data refetched daily or when using cached weather data
- Sunrise API v3.0 with timezone offset parameter
Add server-side SMHI (Swedish Meteorological and Hydrological Institute)
provider with precipitation category handling.

Changes:
- Add smhi_server.js using HTTPFetcher for periodic weather fetching
- Implement gap filling for hourly data interpolation
- Calculate apparent temperature using heat index formula
- Handle precipitation categories (snow, rain, mixed, drizzle, freezing)
- Support configurable precipitation values (pmin, pmean, pmedian, pmax)
- Update weather.js to recognize smhi as server-side provider
- Remove client-side smhi.js (now obsolete)

Implementation notes:
- Sweden only, metric system required
- Coordinate precision limited to 6 decimals per API requirements
- Data gaps filled by duplicating previous hour's data
- Weather type selected from median of daytime hours for forecasts
- Uses SunCalc for sunrise/sunset times
- Updated to new Environment Canada MSC Datamart API structure
  * Changed base URL from /citypage_weather/ to /today/citypage_weather/
  * Updated filename pattern to timestamped format: {timestamp}_MSC_CitypageWeather_{siteCode}_en.xml
- Implemented regex-based XML parsing (no external dependencies)
- Two-step data fetch: index page → city XML file
- Supports current conditions, forecast (12 periods), and hourly (24 hours)
- Features:
  * Wind chill and humidex for feels-like temperature
  * Today/Tonight forecast logic
  * Sunrise/sunset times from XML
  * Weather alerts/warnings support
- All three types tested and working (Toronto)
- Migrated from WeatherProvider.register() to HTTPFetcher-based provider
- Supports current conditions, forecast (daily), and hourly forecasts
- Features:
  * API key validation with helpful error messages
  * Precipitation type detection (rain/snow)
  * Apparent temperature (feels-like) support
  * Sunrise/sunset from daily data
- Tested and working with valid API key
- Migrated from WeatherProvider.register() to HTTPFetcher-based provider
- Supports current conditions, forecast (daily), and hourly (3-hourly) forecasts
- Features:
  * API key validation with helpful error messages
  * SunCalc integration for sunrise/sunset times
  * All 31 Met Office significant weather codes mapped
  * Handles different field names for hourly vs 3-hourly data
  * Temperature averaging for 3-hourly data (max/min avg)
  * Precipitation probabilities for rain, snow, hail
  * Feels-like temperature support
- Tested and working with valid API key (London)
All 10 weather providers now use the same import pattern.
- OpenMeteo: Fix hourly logic that skipped first valid future hour
- OpenMeteo: Add bounds check to prevent array out-of-bounds crash
- Pirateweather: Fix null-safe checks to preserve 0 values
  (humidity, feelsLike, windSpeed, tempMin/Max, precipProb)
- http_fetcher: Wrap URL parsing in try-catch to prevent crash during error logging
- E2E weather-functions: Fix precipitationProbability to use null-safe check
- Electron weather-setup: Fix precipitationProbability to use null-safe check
- Ensures 0% precipitation is preserved instead of becoming undefined
- openmeteo_spec: Remove debug console.log statements
- envcanada_spec: Fix comment (windChill is -31, not -12)
- envcanada_spec: Fix inverted callbacks in error handling test
- smhi_spec: Remove unused SMHI_URL constant
- Split precipitationProbability/uvIndex assignments
- Split precip initialization and if-statement
- Improves readability and follows coding standards
- Fixes inconsistent field names across providers
- Templates expect windFromDirection (current.njk uses it)
- Affected providers: Pirateweather, UkMetOffice, Weatherbit, WeatherFlow
- Ensures wind direction displays correctly in UI
- provider-utils.js: Remove unused Log import
- openmeteo_spec.js: Remove unused beforeEach import and OPEN_METEO_URL constant
- Remove unused errorPromise variable
- Simplify callback setup without unused resolve/reject
- Makes test intent clearer
- Adds back getVersion() function and /version route
- Was incorrectly removed
- /version endpoint is unrelated to weather module
- Restores public API endpoint
- Increase timeout from 5s to 10s for geocoding API
- Change error log level from ERROR to DEBUG
- Geocoding is optional (only for location name display)
Restore original day/night-specific icons and missing codes 41-48
(tornado, windy, smoke, sandstorm, thunderstorm variants) that were
lost during migration.
Rename windDirection → windFromDirection in generateHourly() and
precipitation → precipitationAmount in generateForecast() and
generateHourly() to match WeatherObject properties.
Compare year, month, and day when matching hourly data to daily
forecasts to prevent incorrect matches across month boundaries
(e.g., Jan 31 vs Feb 31).
Use waitForSelector and waitForFunction instead of fixed 1000ms/500ms
delays in weather-setup.js for more robust and faster test execution.
Move describe() and it() opening to separate lines for better readability.
Ensure current.temperature is always set (either to a value, cached value,
or null) to prevent undefined temperature in weather display.
Environment Canada's XML feed now returns <currentConditions/> as an
empty element. Adapt by extracting current weather from the first
forecast period instead.

Additional fixes:
- Add missing return when city page URL not found
- Restore sunrise/sunset data (from riseSet element)
- Accept both "high" and "low" temperature classes
…urrentConditions

Wind speed, bearing, temperature, and humidity now correctly read from
currentConditions element instead of forecast, with fallback logic.
More robust property existence check in PirateWeather provider
instead of value-based undefined check.
…tUrl method

Aligns with existing pattern in openweathermap, weatherapi providers.
…ackoff

Network errors now use exponential backoff strategy instead of fixed
reloadInterval delay, enabling faster recovery from transient issues.

Changes:
- Add networkErrorCount tracking alongside serverErrorCount
- Network errors retry at: 15s → 30s → 60s → cap at reloadInterval
- Gradual log-level escalation: WARN for first 2 attempts, ERROR after
- Extract retry calculation to static HTTPFetcher.calculateBackoffDelay()
- Apply same backoff strategy to WeatherGov initialization retries
- Reset both counters on successful response

Benefits:
- Faster recovery from transient network glitches (15s vs 10min)
- Less log spam for temporary issues (WARN vs ERROR initially)
- Consistent retry behavior across HTTPFetcher and provider init
- Reusable backoff calculation for future providers

Example: SMHI "fetch failed" now retries after 15s instead of 10min.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant