Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
4544bdc
refactor(weather): migrate OpenMeteo provider to server-side with HTT…
KristjanESPERANTO Feb 15, 2026
162c9d8
refactor(weather): migrate OpenWeatherMap provider to server-side
KristjanESPERANTO Feb 15, 2026
07dfd96
refactor(weather): migrate WeatherGov provider to server-side
KristjanESPERANTO Feb 15, 2026
94d52f5
refactor(weather): migrate Yr.no provider to server-side
KristjanESPERANTO Feb 15, 2026
74e47a2
refactor(weather): migrate SMHI provider to server-side
KristjanESPERANTO Feb 15, 2026
d59b5d5
refactor(weather): migrate EnvCanada provider to server-side
KristjanESPERANTO Feb 15, 2026
efd69e4
chore(weather): improve authentication error message for clarity
KristjanESPERANTO Feb 15, 2026
16d6689
refactor(weather): migrate Pirateweather provider to server-side
KristjanESPERANTO Feb 15, 2026
a6c6b80
refactor(weather): migrate UkMetOfficeDataHub provider to server-side
KristjanESPERANTO Feb 15, 2026
031ea10
refactor(weather): add logContext to HTTPFetcher options for better l…
KristjanESPERANTO Feb 15, 2026
df59ad5
refactor(weather): migrate Weatherbit provider to server-side
KristjanESPERANTO Feb 15, 2026
7e07e36
refactor(weather): migrate WeatherFlow provider to server-side
KristjanESPERANTO Feb 15, 2026
2510972
refactor(weather): integrate override logic into weather.js
KristjanESPERANTO Feb 15, 2026
d168d79
docs(weather): update link to MagicMirror² weather provider documenta…
KristjanESPERANTO Feb 15, 2026
c054b8e
refactor(weather): complete server-side migration cleanup
KristjanESPERANTO Feb 15, 2026
060b9d1
refactor(weather): remove unnecessary log prefixes in node_helper.js
KristjanESPERANTO Feb 15, 2026
ca1a0de
fix(weather): update EnvCanada provider for API structure change
KristjanESPERANTO Feb 15, 2026
60e22f2
test(weather): Add weather provider smoke tests
KristjanESPERANTO Feb 15, 2026
cb1bca3
feat(weather): add stopWeatherProvider functionality to manage weathe…
KristjanESPERANTO Feb 15, 2026
8a90f4c
fix(weather): Fix timezone handling in OpenMeteo and Weatherbit
KristjanESPERANTO Feb 15, 2026
e357732
refactor(tests): clean up weather E2E tests with socket injection
KristjanESPERANTO Feb 15, 2026
6cf800c
refactor(tests): migrate Electron weather tests to socket injection
KristjanESPERANTO Feb 15, 2026
a3643fe
refactor(weather): enhance JSDoc comments for callback parameters in …
KristjanESPERANTO Feb 15, 2026
5aaebda
refactor(tests): improve weather module initialization and rendering …
KristjanESPERANTO Feb 15, 2026
1f82392
fix(weather): add switch default cases to prevent undefined callbacks
KristjanESPERANTO Feb 15, 2026
255fd78
fix(openmeteo): use API timezone instead of server timezone for hourl…
KristjanESPERANTO Feb 15, 2026
b62d698
fix(weathergov): convert wind direction from string to degrees
KristjanESPERANTO Feb 15, 2026
7a938a1
fix(yr): use local timezone instead of hardcoded CET for sunrise API
KristjanESPERANTO Feb 15, 2026
4abc975
refactor(openweathermap): validate callbacks before initialize
KristjanESPERANTO Feb 15, 2026
c404a79
refactor(weather): extract shared utilities to reduce code duplication
KristjanESPERANTO Feb 15, 2026
99b8954
test(weather): restore global fetch mock after tests
KristjanESPERANTO Feb 15, 2026
a65346d
fix(envcanada): replace magic number 999 with null for temperature cache
KristjanESPERANTO Feb 15, 2026
25f1338
fix(yr): await stellar data fetch when using cached weather data
KristjanESPERANTO Feb 15, 2026
11dc77b
refactor(weather): centralize limitDecimals utility
KristjanESPERANTO Feb 15, 2026
718ac8c
refactor(ukmetofficedatahub): make internal methods private
KristjanESPERANTO Feb 15, 2026
9d72d17
refactor(openmeteo): simplify property checks
KristjanESPERANTO Feb 15, 2026
e71b835
fix(weather): prevent invalid moment objects from null dates
KristjanESPERANTO Feb 15, 2026
18dc9d8
refactor(weather): rename utils.js to provider-utils.js
KristjanESPERANTO Feb 15, 2026
c0c85d5
fix(weather): handle missing sunrise/sunset data gracefully
KristjanESPERANTO Feb 15, 2026
58cdaa5
test(weather): add unit tests for provider-utils
KristjanESPERANTO Feb 15, 2026
8a4498d
fix(weather): use local time in getDateString instead of UTC
KristjanESPERANTO Feb 15, 2026
ebecaac
fix(weatherbit): use data timestamp for sunrise/sunset date
KristjanESPERANTO Feb 15, 2026
8f48c43
fix(envcanada): prevent double timezone shift in date parsing
KristjanESPERANTO Feb 15, 2026
cdb12ff
fix(openmeteo): handle both hourly data shapes in current weather
KristjanESPERANTO Feb 15, 2026
53582ee
chore(eslint): allow loose equality for null checks
KristjanESPERANTO Feb 15, 2026
b80b1e8
fix(smhi): correct gap-filling algorithm to preserve data
KristjanESPERANTO Feb 15, 2026
7d70370
fix(ukmetofficedatahub): use precipitationAmount instead of precipita…
KristjanESPERANTO Feb 15, 2026
b70e49a
fix(weathergov): remove incorrect wind conversion for observations
KristjanESPERANTO Feb 15, 2026
e176854
fix(yr): add default case to weather type switch
KristjanESPERANTO Feb 15, 2026
7fab859
test(weather): add validation for mock weather data fixtures
KristjanESPERANTO Feb 15, 2026
0c9a7c1
test(server_functions): restore global.config after test
KristjanESPERANTO Feb 15, 2026
c1a9ab4
refactor(weather): centralize common utility functions
KristjanESPERANTO Feb 15, 2026
869be0a
fix(weather): add default cases to all provider switch statements
KristjanESPERANTO Feb 15, 2026
06a5234
fix(weather): add error handling to smhi initialize
KristjanESPERANTO Feb 15, 2026
0c80359
fix(weather): add defensive null checks to weatherflow provider
KristjanESPERANTO Feb 15, 2026
8f8cad2
refactor(weather): code quality improvements from nitpick review
KristjanESPERANTO Feb 15, 2026
34137df
fix(weather): additional nitpick improvements
KristjanESPERANTO Feb 15, 2026
46179d9
fix(weather): correct WeatherFlow provider event handling and data st…
KristjanESPERANTO Feb 15, 2026
2d0f89f
fix(weather): improve Yr.no daily forecast data aggregation
KristjanESPERANTO Feb 15, 2026
960bee6
fix(weather): EnvCanada hourly timestamps and null value handling
KristjanESPERANTO Feb 15, 2026
3f342ca
fix(weather): change log level from warn to debug for missing hourly …
KristjanESPERANTO Feb 15, 2026
2389502
fix(weather): improve null value handling and error logging
KristjanESPERANTO Feb 15, 2026
245dc8f
fix(weather): increase Weather.gov timeout for reliability
KristjanESPERANTO Feb 15, 2026
54f85d2
chore(weather): simplify log prefixes for weather providers
KristjanESPERANTO Feb 15, 2026
8b2fd36
fix(weather): transform Yr.no stellar data into expected array format
KristjanESPERANTO Feb 15, 2026
409d907
fix(weather): correct OpenMeteo daily data access after transpose
KristjanESPERANTO Feb 15, 2026
c280d67
fix(weather): preserve 0% precipitation probability in OpenWeatherMap
KristjanESPERANTO Feb 15, 2026
5941092
fix(weather): use config values for units and lang in Pirateweather
KristjanESPERANTO Feb 15, 2026
3295851
fix(weather): prevent undefined data callback in SMHI default case
KristjanESPERANTO Feb 15, 2026
570833f
fix(weather): use correct property name precipitationAmount in Weathe…
KristjanESPERANTO Feb 15, 2026
e15ea06
fix(weather): add null-check for wind_avg in WeatherFlow
KristjanESPERANTO Feb 15, 2026
cae1fe8
fix(weather): add error callbacks for unknown weather types
KristjanESPERANTO Feb 15, 2026
1674363
fix(weather): use timestamp comparison in OpenMeteo time matching
KristjanESPERANTO Feb 15, 2026
c6483f6
style(tests): fix formatting in hourlyweather_default config
KristjanESPERANTO Feb 15, 2026
488501e
feat(weather): add retry logic and prevent parallel DNS lookups
KristjanESPERANTO Feb 15, 2026
f73fca3
fix(weather): reduce log noise in EnvCanada during hour transitions
KristjanESPERANTO Feb 15, 2026
8bcbd76
test: add comprehensive unit tests for weather providers
KristjanESPERANTO Feb 15, 2026
2a77aef
fix(weather): handle null values in OpenMeteo provider
KristjanESPERANTO Feb 15, 2026
3508e01
fix(weather): add missing icon code 40 to EnvCanada provider
KristjanESPERANTO Feb 15, 2026
189a2f7
refactor(weather): use #http_fetcher alias in WeatherFlow provider
KristjanESPERANTO Feb 15, 2026
0d57cd8
fix(weather): fix provider bugs from CodeRabbit review
KristjanESPERANTO Feb 15, 2026
fe0161e
fix(core): improve error handling and remove dead code
KristjanESPERANTO Feb 15, 2026
7b5bd65
fix(tests): preserve 0 values in test helpers
KristjanESPERANTO Feb 15, 2026
f300949
chore(tests): clean up test specs
KristjanESPERANTO Feb 15, 2026
9d27f04
style(weather): split multi-statement lines in OpenWeatherMap
KristjanESPERANTO Feb 15, 2026
6284897
fix(weather): rename windDirection to windFromDirection
KristjanESPERANTO Feb 15, 2026
d05aff1
chore: remove unused imports
KristjanESPERANTO Feb 15, 2026
56a20a8
refactor(tests): simplify EnvCanada error test
KristjanESPERANTO Feb 15, 2026
5ee079c
fix(server): restore /version endpoint
KristjanESPERANTO Feb 15, 2026
9fa9995
fix(weather): increase OpenMeteo geocoding timeout and reduce log noise
KristjanESPERANTO Feb 15, 2026
1c760b5
fix(envcanada): restore complete weatherType map (codes 0-48)
KristjanESPERANTO Feb 15, 2026
29b9b75
fix(pirateweather): use correct WeatherObject field names
KristjanESPERANTO Feb 15, 2026
f349991
fix(weatherflow): compare full date instead of day-of-month
KristjanESPERANTO Feb 15, 2026
02b0c05
refactor(tests): replace waitForTimeout with deterministic waits
KristjanESPERANTO Feb 15, 2026
c6c9439
style(tests): break multi-statement line in ukmetoffice test
KristjanESPERANTO Feb 15, 2026
bb50cad
fix(envcanada): set temperature to null when unavailable
KristjanESPERANTO Feb 15, 2026
9eed95e
fix(envcanada): handle empty currentConditions element
KristjanESPERANTO Feb 15, 2026
9c0c06b
revert: restore CORS proxy for newsfeed and 3rd-party module compatib…
KristjanESPERANTO Feb 15, 2026
f6ae5c7
tests(weather): fix EnvCanada provider to read current weather from c…
KristjanESPERANTO Feb 15, 2026
33e8eaa
refactor: use hasOwnProperty for precipAccumulation check
KristjanESPERANTO Feb 15, 2026
2807859
feat(ukmetofficedatahub): add withFutureDailyTimes function to shift …
KristjanESPERANTO Feb 15, 2026
6945854
refactor(weather): change method visibility to private
KristjanESPERANTO Feb 15, 2026
7ee5d42
refactor(pirateweather): change default units from 'us' to 'si' in ge…
KristjanESPERANTO Feb 15, 2026
0b041d9
feat(http_fetcher): improve network error handling with exponential b…
KristjanESPERANTO Feb 15, 2026
88c49b4
fix(weatherbit): fix weather icon mappings
KristjanESPERANTO Feb 15, 2026
d54caa6
feat(weatherapi): implement WeatherAPIProvider for current, daily, an…
KristjanESPERANTO Feb 15, 2026
be9c24a
refactor(weather): extract cardinalToDegrees() to provider-utils.js
KristjanESPERANTO Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 0 additions & 152 deletions defaultmodules/utils.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,3 @@
/**
* A function to make HTTP requests via the server to avoid CORS-errors.
* @param {string} url the url to fetch from
* @param {string} type what content-type to expect in the response, can be "json" or "xml"
* @param {boolean} useCorsProxy A flag to indicate
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {string} basePath The base path, default is "/"
* @returns {Promise} resolved when the fetch is done. The response headers is placed in a headers-property (provided the response does not already contain a headers-property).
*/
async function performWebRequest (url, type = "json", useCorsProxy = false, requestHeaders = undefined, expectedResponseHeaders = undefined, basePath = "/") {
const request = {};
let requestUrl;
if (useCorsProxy) {
requestUrl = getCorsUrl(url, requestHeaders, expectedResponseHeaders, basePath);
} else {
requestUrl = url;
request.headers = getHeadersToSend(requestHeaders);
}

try {
const response = await fetch(requestUrl, request);
if (response.ok) {
const data = await response.text();

if (type === "xml") {
return new DOMParser().parseFromString(data, "text/html");
} else {
if (!data || !data.length > 0) return undefined;

const dataResponse = JSON.parse(data);
if (!dataResponse.headers) {
dataResponse.headers = getHeadersFromResponse(expectedResponseHeaders, response);
}
return dataResponse;
}
} else {
throw new Error(`Response status: ${response.status}`);
}
} catch (error) {
Log.error(`Error fetching data from ${url}: ${error}`);
return undefined;
}
}

/**
* Gets a URL that will be used when calling the CORS-method on the server.
* @param {string} url the url to fetch from
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {string} basePath The base path, default is "/"
* @returns {string} to be used as URL when calling CORS-method on server.
*/
const getCorsUrl = function (url, requestHeaders, expectedResponseHeaders, basePath = "/") {
if (!url || url.length < 1) {
throw new Error(`Invalid URL: ${url}`);
} else {
let corsUrl = `${location.protocol}//${location.host}${basePath}cors?`;

const requestHeaderString = getRequestHeaderString(requestHeaders);
if (requestHeaderString) corsUrl = `${corsUrl}sendheaders=${requestHeaderString}`;

const expectedResponseHeadersString = getExpectedResponseHeadersString(expectedResponseHeaders);
if (requestHeaderString && expectedResponseHeadersString) {
corsUrl = `${corsUrl}&expectedheaders=${expectedResponseHeadersString}`;
} else if (expectedResponseHeadersString) {
corsUrl = `${corsUrl}expectedheaders=${expectedResponseHeadersString}`;
}

if (requestHeaderString || expectedResponseHeadersString) {
return `${corsUrl}&url=${url}`;
}
return `${corsUrl}url=${url}`;
}
};

/**
* Gets the part of the CORS URL that represents the HTTP headers to send.
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {string} to be used as request-headers component in CORS URL.
*/
const getRequestHeaderString = function (requestHeaders) {
let requestHeaderString = "";
if (requestHeaders) {
for (const header of requestHeaders) {
if (requestHeaderString.length === 0) {
requestHeaderString = `${header.name}:${encodeURIComponent(header.value)}`;
} else {
requestHeaderString = `${requestHeaderString},${header.name}:${encodeURIComponent(header.value)}`;
}
}
return requestHeaderString;
}
return undefined;
};

/**
* Gets headers and values to attach to the web request.
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @returns {object} An object specifying name and value of the headers.
*/
const getHeadersToSend = (requestHeaders) => {
const headersToSend = {};
if (requestHeaders) {
for (const header of requestHeaders) {
headersToSend[header.name] = header.value;
}
}

return headersToSend;
};

/**
* Gets the part of the CORS URL that represents the expected HTTP headers to receive.
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/
const getExpectedResponseHeadersString = function (expectedResponseHeaders) {
let expectedResponseHeadersString = "";
if (expectedResponseHeaders) {
for (const header of expectedResponseHeaders) {
if (expectedResponseHeadersString.length === 0) {
expectedResponseHeadersString = `${header}`;
} else {
expectedResponseHeadersString = `${expectedResponseHeadersString},${header}`;
}
}
return expectedResponseHeaders;
}
return undefined;
};

/**
* Gets the values for the expected headers from the response.
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to receive
* @param {Response} response the HTTP response
* @returns {string} to be used as the expected HTTP-headers component in CORS URL.
*/
const getHeadersFromResponse = (expectedResponseHeaders, response) => {
const responseHeaders = [];

if (expectedResponseHeaders) {
for (const header of expectedResponseHeaders) {
const headerValue = response.headers.get(header);
responseHeaders.push({ name: header, value: headerValue });
}
}

return responseHeaders;
};

/**
* Format the time according to the config
* @param {object} config The config of the module
Expand Down Expand Up @@ -178,6 +27,5 @@ const formatTime = (config, time) => {
};

if (typeof module !== "undefined") module.exports = {
performWebRequest,
formatTime
};
2 changes: 1 addition & 1 deletion defaultmodules/weather/current.njk
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
{% if config.showHumidity === "wind" %}
{{ humidity() }}
{% endif %}
{% if config.showSun %}
{% if config.showSun and current.nextSunAction() %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
<span>
{% if current.nextSunAction() === "sunset" %}
Expand Down
103 changes: 103 additions & 0 deletions defaultmodules/weather/node_helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const path = require("node:path");
const NodeHelper = require("node_helper");
const Log = require("logger");

module.exports = NodeHelper.create({
providers: {},

start () {
Log.log(`Starting node helper for: ${this.name}`);
},

socketNotificationReceived (notification, payload) {
if (notification === "INIT_WEATHER") {
Log.log(`Received INIT_WEATHER for instance ${payload.instanceId}`);
this.initWeatherProvider(payload);
} else if (notification === "STOP_WEATHER") {
Log.log(`Received STOP_WEATHER for instance ${payload.instanceId}`);
this.stopWeatherProvider(payload.instanceId);
}
// FETCH_WEATHER is no longer needed - HTTPFetcher handles periodic fetching
},

/**
* Initialize a weather provider
* @param {object} config The configuration object
*/
async initWeatherProvider (config) {
const identifier = config.weatherProvider.toLowerCase();
const instanceId = config.instanceId;

Log.log(`Attempting to initialize provider ${identifier} for instance ${instanceId}`);

if (this.providers[instanceId]) {
Log.log(`Weather provider ${identifier} already initialized for instance ${instanceId}`);
return;
}

try {
// Dynamically load the provider module
const providerPath = path.join(__dirname, "providers", `${identifier}.js`);
Log.log(`Loading provider from: ${providerPath}`);
const ProviderClass = require(providerPath);

// Create provider instance
const provider = new ProviderClass(config);

// Set up callbacks before initializing
provider.setCallbacks(
(data) => {
// On data received
this.sendSocketNotification("WEATHER_DATA", {
instanceId,
type: config.type,
data
});
},
(errorInfo) => {
// On error
this.sendSocketNotification("WEATHER_ERROR", {
instanceId,
error: errorInfo.message || "Unknown error",
translationKey: errorInfo.translationKey
});
}
);

await provider.initialize();
this.providers[instanceId] = provider;

this.sendSocketNotification("WEATHER_INITIALIZED", {
instanceId,
locationName: provider.locationName
});

// Start periodic fetching
provider.start();

Log.log(`Weather provider ${identifier} initialized for instance ${instanceId}`);
} catch (error) {
Log.error(`Failed to initialize weather provider ${identifier}:`, error);
this.sendSocketNotification("WEATHER_ERROR", {
instanceId,
error: error.message
});
}
},

/**
* Stop and cleanup a weather provider
* @param {string} instanceId The instance identifier
*/
stopWeatherProvider (instanceId) {
const provider = this.providers[instanceId];

if (provider) {
Log.log(`Stopping weather provider for instance ${instanceId}`);
provider.stop();
delete this.providers[instanceId];
} else {
Log.warn(`No provider found for instance ${instanceId}`);
}
}
});
Loading