Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
cbe3442
refactor(weather): migrate OpenMeteo provider to server-side with HTT…
KristjanESPERANTO Feb 8, 2026
62eaaef
refactor(weather): migrate OpenWeatherMap provider to server-side
KristjanESPERANTO Feb 8, 2026
48d02c5
refactor(weather): migrate WeatherGov provider to server-side
KristjanESPERANTO Feb 8, 2026
f605d72
refactor(weather): migrate Yr.no provider to server-side
KristjanESPERANTO Feb 8, 2026
4f4404c
refactor(weather): migrate SMHI provider to server-side
KristjanESPERANTO Feb 8, 2026
7550424
refactor(weather): migrate EnvCanada provider to server-side
KristjanESPERANTO Feb 8, 2026
6692366
chore(weather): improve authentication error message for clarity
KristjanESPERANTO Feb 8, 2026
fa36bf5
refactor(weather): migrate Pirateweather provider to server-side
KristjanESPERANTO Feb 8, 2026
c2c8c6f
refactor(weather): migrate UkMetOfficeDataHub provider to server-side
KristjanESPERANTO Feb 8, 2026
67cff3b
refactor(weather): add logContext to HTTPFetcher options for better l…
KristjanESPERANTO Feb 8, 2026
b781286
refactor(weather): migrate Weatherbit provider to server-side
KristjanESPERANTO Feb 8, 2026
e53d0b7
refactor(weather): migrate WeatherFlow provider to server-side
KristjanESPERANTO Feb 8, 2026
aa46510
refactor(weather): integrate override logic into weather.js
KristjanESPERANTO Feb 8, 2026
2b5f86d
docs(weather): update link to MagicMirror² weather provider documenta…
KristjanESPERANTO Feb 8, 2026
4144d36
refactor(weather): complete server-side migration cleanup
KristjanESPERANTO Feb 8, 2026
73e4e6b
refactor(weather): remove unnecessary log prefixes in node_helper.js
KristjanESPERANTO Feb 8, 2026
8d94ac0
fix(weather): update EnvCanada provider for API structure change
KristjanESPERANTO Feb 8, 2026
1972477
test(weather): Add weather provider smoke tests
KristjanESPERANTO Feb 8, 2026
cdeb72b
feat(weather): add stopWeatherProvider functionality to manage weathe…
KristjanESPERANTO Feb 8, 2026
637e67c
fix(weather): Fix timezone handling in OpenMeteo and Weatherbit
KristjanESPERANTO Feb 8, 2026
c88053f
refactor(tests): clean up weather E2E tests with socket injection
KristjanESPERANTO Feb 8, 2026
862e461
refactor(tests): migrate Electron weather tests to socket injection
KristjanESPERANTO Feb 8, 2026
5cf7de2
refactor(weather): enhance JSDoc comments for callback parameters in …
KristjanESPERANTO Feb 8, 2026
33b72f7
refactor(tests): improve weather module initialization and rendering …
KristjanESPERANTO Feb 8, 2026
4ed010c
fix(weather): add switch default cases to prevent undefined callbacks
KristjanESPERANTO Feb 8, 2026
39528e8
fix(openmeteo): use API timezone instead of server timezone for hourl…
KristjanESPERANTO Feb 8, 2026
aa63f8b
fix(weathergov): convert wind direction from string to degrees
KristjanESPERANTO Feb 8, 2026
d110778
fix(yr): use local timezone instead of hardcoded CET for sunrise API
KristjanESPERANTO Feb 8, 2026
0f9cad2
refactor(openweathermap): validate callbacks before initialize
KristjanESPERANTO Feb 8, 2026
4a3c725
refactor(weather): extract shared utilities to reduce code duplication
KristjanESPERANTO Feb 8, 2026
2a6bcb9
test(weather): restore global fetch mock after tests
KristjanESPERANTO Feb 8, 2026
6e07b93
fix(envcanada): replace magic number 999 with null for temperature cache
KristjanESPERANTO Feb 8, 2026
46533d3
fix(yr): await stellar data fetch when using cached weather data
KristjanESPERANTO Feb 8, 2026
84468f6
refactor(weather): centralize limitDecimals utility
KristjanESPERANTO Feb 8, 2026
dd8c3b8
refactor(ukmetofficedatahub): make internal methods private
KristjanESPERANTO Feb 8, 2026
1814d34
refactor(openmeteo): simplify property checks
KristjanESPERANTO Feb 8, 2026
07d6135
fix(weather): prevent invalid moment objects from null dates
KristjanESPERANTO Feb 8, 2026
27da2d5
refactor(weather): rename utils.js to provider-utils.js
KristjanESPERANTO Feb 8, 2026
d99f6db
fix(weather): handle missing sunrise/sunset data gracefully
KristjanESPERANTO Feb 8, 2026
bbad291
test(weather): add unit tests for provider-utils
KristjanESPERANTO Feb 8, 2026
1537fde
fix(weather): use local time in getDateString instead of UTC
KristjanESPERANTO Feb 8, 2026
c5c5188
fix(weatherbit): use data timestamp for sunrise/sunset date
KristjanESPERANTO Feb 8, 2026
f353634
fix(envcanada): prevent double timezone shift in date parsing
KristjanESPERANTO Feb 8, 2026
ddb85f3
fix(openmeteo): handle both hourly data shapes in current weather
KristjanESPERANTO Feb 8, 2026
a2c3067
chore(eslint): allow loose equality for null checks
KristjanESPERANTO Feb 8, 2026
6f59ef0
fix(smhi): correct gap-filling algorithm to preserve data
KristjanESPERANTO Feb 8, 2026
9578c0d
fix(ukmetofficedatahub): use precipitationAmount instead of precipita…
KristjanESPERANTO Feb 8, 2026
1059c25
fix(weathergov): remove incorrect wind conversion for observations
KristjanESPERANTO Feb 8, 2026
ac87a4f
fix(yr): add default case to weather type switch
KristjanESPERANTO Feb 8, 2026
3670008
test(weather): add validation for mock weather data fixtures
KristjanESPERANTO Feb 8, 2026
6a5c492
test(server_functions): restore global.config after test
KristjanESPERANTO Feb 8, 2026
fe218cf
refactor(weather): centralize common utility functions
KristjanESPERANTO Feb 8, 2026
cf83c71
fix(weather): add default cases to all provider switch statements
KristjanESPERANTO Feb 8, 2026
906614c
fix(weather): add error handling to smhi initialize
KristjanESPERANTO Feb 8, 2026
5164d1b
fix(weather): add defensive null checks to weatherflow provider
KristjanESPERANTO Feb 8, 2026
551549f
refactor(weather): code quality improvements from nitpick review
KristjanESPERANTO Feb 8, 2026
c8012ed
fix(weather): additional nitpick improvements
KristjanESPERANTO Feb 8, 2026
0de5eaa
fix(weather): correct WeatherFlow provider event handling and data st…
KristjanESPERANTO Feb 8, 2026
dafb524
fix(weather): improve Yr.no daily forecast data aggregation
KristjanESPERANTO Feb 8, 2026
8cf4713
fix(weather): EnvCanada hourly timestamps and null value handling
KristjanESPERANTO Feb 8, 2026
ed7c828
fix(weather): change log level from warn to debug for missing hourly …
KristjanESPERANTO Feb 8, 2026
652fd66
fix(weather): improve null value handling and error logging
KristjanESPERANTO Feb 8, 2026
6222ab7
fix(weather): increase Weather.gov timeout for reliability
KristjanESPERANTO Feb 8, 2026
82efb42
chore(weather): simplify log prefixes for weather providers
KristjanESPERANTO Feb 8, 2026
e06248b
fix(weather): transform Yr.no stellar data into expected array format
KristjanESPERANTO Feb 8, 2026
fc60f14
fix(weather): correct OpenMeteo daily data access after transpose
KristjanESPERANTO Feb 8, 2026
5f4a996
fix(weather): preserve 0% precipitation probability in OpenWeatherMap
KristjanESPERANTO Feb 8, 2026
71202b2
fix(weather): use config values for units and lang in Pirateweather
KristjanESPERANTO Feb 8, 2026
3f088b5
fix(weather): prevent undefined data callback in SMHI default case
KristjanESPERANTO Feb 8, 2026
e4d2a3b
fix(weather): use correct property name precipitationAmount in Weathe…
KristjanESPERANTO Feb 8, 2026
2ffc6a2
fix(weather): add null-check for wind_avg in WeatherFlow
KristjanESPERANTO Feb 8, 2026
7a335d8
fix(weather): add error callbacks for unknown weather types
KristjanESPERANTO Feb 8, 2026
1aea543
fix(weather): use timestamp comparison in OpenMeteo time matching
KristjanESPERANTO Feb 8, 2026
adbb7bb
style(tests): fix formatting in hourlyweather_default config
KristjanESPERANTO Feb 8, 2026
9fd90c4
feat(weather): add retry logic and prevent parallel DNS lookups
KristjanESPERANTO Feb 8, 2026
16c5e4b
fix(weather): reduce log noise in EnvCanada during hour transitions
KristjanESPERANTO Feb 8, 2026
ef9c9d8
test: add comprehensive unit tests for weather providers
KristjanESPERANTO Feb 8, 2026
6dd3bea
fix(weather): handle null values in OpenMeteo provider
KristjanESPERANTO Feb 8, 2026
53e4dd5
fix(weather): add missing icon code 40 to EnvCanada provider
KristjanESPERANTO Feb 8, 2026
45e3b5b
refactor(weather): use #http_fetcher alias in WeatherFlow provider
KristjanESPERANTO Feb 8, 2026
8ed477a
fix(weather): fix provider bugs from CodeRabbit review
KristjanESPERANTO Feb 8, 2026
ba014ca
fix(core): improve error handling and remove dead code
KristjanESPERANTO Feb 8, 2026
c4fa60c
fix(tests): preserve 0 values in test helpers
KristjanESPERANTO Feb 8, 2026
cc6f273
chore(tests): clean up test specs
KristjanESPERANTO Feb 8, 2026
8d94876
style(weather): split multi-statement lines in OpenWeatherMap
KristjanESPERANTO Feb 8, 2026
1892c5c
fix(weather): rename windDirection to windFromDirection
KristjanESPERANTO Feb 8, 2026
e897aa9
chore: remove unused imports
KristjanESPERANTO Feb 8, 2026
343e6fa
refactor(tests): simplify EnvCanada error test
KristjanESPERANTO Feb 8, 2026
7b710af
fix(server): restore /version endpoint
KristjanESPERANTO Feb 8, 2026
3f249ed
fix(weather): increase OpenMeteo geocoding timeout and reduce log noise
KristjanESPERANTO Feb 8, 2026
11f013d
fix(envcanada): restore complete weatherType map (codes 0-48)
KristjanESPERANTO Feb 8, 2026
46f2462
fix(pirateweather): use correct WeatherObject field names
KristjanESPERANTO Feb 8, 2026
ef13505
fix(weatherflow): compare full date instead of day-of-month
KristjanESPERANTO Feb 8, 2026
d159d07
refactor(tests): replace waitForTimeout with deterministic waits
KristjanESPERANTO Feb 8, 2026
ab25cf4
style(tests): break multi-statement line in ukmetoffice test
KristjanESPERANTO Feb 8, 2026
e417f32
fix(envcanada): set temperature to null when unavailable
KristjanESPERANTO Feb 8, 2026
4a851d7
fix(envcanada): handle empty currentConditions element
KristjanESPERANTO Feb 8, 2026
310a5ac
revert: restore CORS proxy for newsfeed and 3rd-party module compatib…
KristjanESPERANTO Feb 8, 2026
3ceb5b4
tests(weather): fix EnvCanada provider to read current weather from c…
KristjanESPERANTO Feb 8, 2026
e13d17e
refactor: use hasOwnProperty for precipAccumulation check
KristjanESPERANTO Feb 8, 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