Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
cf15b22
refactor(weather): migrate OpenMeteo provider to server-side with HTT…
KristjanESPERANTO Feb 21, 2026
e517e87
refactor(weather): migrate OpenWeatherMap provider to server-side
KristjanESPERANTO Feb 21, 2026
721648f
refactor(weather): migrate WeatherGov provider to server-side
KristjanESPERANTO Feb 21, 2026
801696a
refactor(weather): migrate Yr.no provider to server-side
KristjanESPERANTO Feb 21, 2026
978ff9b
refactor(weather): migrate SMHI provider to server-side
KristjanESPERANTO Feb 21, 2026
76de3ed
refactor(weather): migrate EnvCanada provider to server-side
KristjanESPERANTO Feb 21, 2026
18bd937
chore(weather): improve authentication error message for clarity
KristjanESPERANTO Feb 21, 2026
53555d2
refactor(weather): migrate Pirateweather provider to server-side
KristjanESPERANTO Feb 21, 2026
ee55c0e
refactor(weather): migrate UkMetOfficeDataHub provider to server-side
KristjanESPERANTO Feb 21, 2026
fd0ccac
refactor(weather): add logContext to HTTPFetcher options for better l…
KristjanESPERANTO Feb 21, 2026
5983650
refactor(weather): migrate Weatherbit provider to server-side
KristjanESPERANTO Feb 21, 2026
a278b41
refactor(weather): migrate WeatherFlow provider to server-side
KristjanESPERANTO Feb 21, 2026
53fc6e5
refactor(weather): integrate override logic into weather.js
KristjanESPERANTO Feb 21, 2026
a88a664
docs(weather): update link to MagicMirror² weather provider documenta…
KristjanESPERANTO Feb 21, 2026
42b639c
refactor(weather): complete server-side migration cleanup
KristjanESPERANTO Feb 21, 2026
081c25d
refactor(weather): remove unnecessary log prefixes in node_helper.js
KristjanESPERANTO Feb 21, 2026
9074e72
fix(weather): update EnvCanada provider for API structure change
KristjanESPERANTO Feb 21, 2026
4875738
test(weather): Add weather provider smoke tests
KristjanESPERANTO Feb 21, 2026
aed1f5f
feat(weather): add stopWeatherProvider functionality to manage weathe…
KristjanESPERANTO Feb 21, 2026
6243ccf
fix(weather): Fix timezone handling in OpenMeteo and Weatherbit
KristjanESPERANTO Feb 21, 2026
f1ae597
refactor(tests): clean up weather E2E tests with socket injection
KristjanESPERANTO Feb 21, 2026
1768e19
refactor(tests): migrate Electron weather tests to socket injection
KristjanESPERANTO Feb 21, 2026
da0a585
refactor(weather): enhance JSDoc comments for callback parameters in …
KristjanESPERANTO Feb 21, 2026
c840933
refactor(tests): improve weather module initialization and rendering …
KristjanESPERANTO Feb 21, 2026
109fda0
fix(weather): add switch default cases to prevent undefined callbacks
KristjanESPERANTO Feb 21, 2026
c07b90b
fix(openmeteo): use API timezone instead of server timezone for hourl…
KristjanESPERANTO Feb 21, 2026
3e0d35d
fix(weathergov): convert wind direction from string to degrees
KristjanESPERANTO Feb 21, 2026
98e3cab
fix(yr): use local timezone instead of hardcoded CET for sunrise API
KristjanESPERANTO Feb 21, 2026
ee49dd9
refactor(openweathermap): validate callbacks before initialize
KristjanESPERANTO Feb 21, 2026
33b17ff
refactor(weather): extract shared utilities to reduce code duplication
KristjanESPERANTO Feb 21, 2026
5fcf8ec
test(weather): restore global fetch mock after tests
KristjanESPERANTO Feb 21, 2026
16a4e92
fix(envcanada): replace magic number 999 with null for temperature cache
KristjanESPERANTO Feb 21, 2026
30319b9
fix(yr): await stellar data fetch when using cached weather data
KristjanESPERANTO Feb 21, 2026
27a9d36
refactor(weather): centralize limitDecimals utility
KristjanESPERANTO Feb 21, 2026
ec810ea
refactor(ukmetofficedatahub): make internal methods private
KristjanESPERANTO Feb 21, 2026
1af4ce5
refactor(openmeteo): simplify property checks
KristjanESPERANTO Feb 21, 2026
d9e6481
fix(weather): prevent invalid moment objects from null dates
KristjanESPERANTO Feb 21, 2026
cf5373f
refactor(weather): rename utils.js to provider-utils.js
KristjanESPERANTO Feb 21, 2026
4aa63f7
fix(weather): handle missing sunrise/sunset data gracefully
KristjanESPERANTO Feb 21, 2026
346a65b
test(weather): add unit tests for provider-utils
KristjanESPERANTO Feb 21, 2026
adbc7cf
fix(weather): use local time in getDateString instead of UTC
KristjanESPERANTO Feb 21, 2026
e28dd80
fix(weatherbit): use data timestamp for sunrise/sunset date
KristjanESPERANTO Feb 21, 2026
ef30dbf
fix(envcanada): prevent double timezone shift in date parsing
KristjanESPERANTO Feb 21, 2026
636cd19
fix(openmeteo): handle both hourly data shapes in current weather
KristjanESPERANTO Feb 21, 2026
82e54dc
chore(eslint): allow loose equality for null checks
KristjanESPERANTO Feb 21, 2026
a62a54d
fix(smhi): correct gap-filling algorithm to preserve data
KristjanESPERANTO Feb 21, 2026
08a58e3
fix(ukmetofficedatahub): use precipitationAmount instead of precipita…
KristjanESPERANTO Feb 21, 2026
6517be2
fix(weathergov): remove incorrect wind conversion for observations
KristjanESPERANTO Feb 21, 2026
bfc21f7
fix(yr): add default case to weather type switch
KristjanESPERANTO Feb 21, 2026
b80dc7d
test(weather): add validation for mock weather data fixtures
KristjanESPERANTO Feb 21, 2026
c07b6a6
test(server_functions): restore global.config after test
KristjanESPERANTO Feb 21, 2026
0c8cfa5
refactor(weather): centralize common utility functions
KristjanESPERANTO Feb 21, 2026
b48d970
fix(weather): add default cases to all provider switch statements
KristjanESPERANTO Feb 21, 2026
d8e1221
fix(weather): add error handling to smhi initialize
KristjanESPERANTO Feb 21, 2026
a72e255
fix(weather): add defensive null checks to weatherflow provider
KristjanESPERANTO Feb 21, 2026
fb3ecb7
refactor(weather): code quality improvements from nitpick review
KristjanESPERANTO Feb 21, 2026
610b4d2
fix(weather): additional nitpick improvements
KristjanESPERANTO Feb 21, 2026
0bd8544
fix(weather): correct WeatherFlow provider event handling and data st…
KristjanESPERANTO Feb 21, 2026
5eadac4
fix(weather): improve Yr.no daily forecast data aggregation
KristjanESPERANTO Feb 21, 2026
a19753f
fix(weather): EnvCanada hourly timestamps and null value handling
KristjanESPERANTO Feb 21, 2026
7b3e8e9
fix(weather): change log level from warn to debug for missing hourly …
KristjanESPERANTO Feb 21, 2026
754b65f
fix(weather): improve null value handling and error logging
KristjanESPERANTO Feb 21, 2026
4e5f3fa
fix(weather): increase Weather.gov timeout for reliability
KristjanESPERANTO Feb 21, 2026
ebcc747
chore(weather): simplify log prefixes for weather providers
KristjanESPERANTO Feb 21, 2026
e21c927
fix(weather): transform Yr.no stellar data into expected array format
KristjanESPERANTO Feb 21, 2026
19965a2
fix(weather): correct OpenMeteo daily data access after transpose
KristjanESPERANTO Feb 21, 2026
e82f8b9
fix(weather): preserve 0% precipitation probability in OpenWeatherMap
KristjanESPERANTO Feb 21, 2026
17de4fd
fix(weather): use config values for units and lang in Pirateweather
KristjanESPERANTO Feb 21, 2026
889ce89
fix(weather): prevent undefined data callback in SMHI default case
KristjanESPERANTO Feb 21, 2026
a14a408
fix(weather): use correct property name precipitationAmount in Weathe…
KristjanESPERANTO Feb 21, 2026
2c51cc3
fix(weather): add null-check for wind_avg in WeatherFlow
KristjanESPERANTO Feb 21, 2026
8bcfeb6
fix(weather): add error callbacks for unknown weather types
KristjanESPERANTO Feb 21, 2026
7dfeb42
fix(weather): use timestamp comparison in OpenMeteo time matching
KristjanESPERANTO Feb 21, 2026
b50079e
style(tests): fix formatting in hourlyweather_default config
KristjanESPERANTO Feb 21, 2026
d05192f
feat(weather): add retry logic and prevent parallel DNS lookups
KristjanESPERANTO Feb 21, 2026
c499d3a
fix(weather): reduce log noise in EnvCanada during hour transitions
KristjanESPERANTO Feb 21, 2026
32d493b
test: add comprehensive unit tests for weather providers
KristjanESPERANTO Feb 21, 2026
c7fc9c3
fix(weather): handle null values in OpenMeteo provider
KristjanESPERANTO Feb 21, 2026
12f2f7d
fix(weather): add missing icon code 40 to EnvCanada provider
KristjanESPERANTO Feb 21, 2026
4b68b19
refactor(weather): use #http_fetcher alias in WeatherFlow provider
KristjanESPERANTO Feb 21, 2026
d54a791
fix(weather): fix provider bugs from CodeRabbit review
KristjanESPERANTO Feb 21, 2026
101865c
fix(core): improve error handling and remove dead code
KristjanESPERANTO Feb 21, 2026
79d64f3
fix(tests): preserve 0 values in test helpers
KristjanESPERANTO Feb 21, 2026
4332ee7
chore(tests): clean up test specs
KristjanESPERANTO Feb 21, 2026
9bc7af5
style(weather): split multi-statement lines in OpenWeatherMap
KristjanESPERANTO Feb 21, 2026
59c7a3a
fix(weather): rename windDirection to windFromDirection
KristjanESPERANTO Feb 21, 2026
1475ffe
chore: remove unused imports
KristjanESPERANTO Feb 21, 2026
6b30c39
refactor(tests): simplify EnvCanada error test
KristjanESPERANTO Feb 21, 2026
f18f1f2
fix(server): restore /version endpoint
KristjanESPERANTO Feb 21, 2026
2640577
fix(weather): increase OpenMeteo geocoding timeout and reduce log noise
KristjanESPERANTO Feb 21, 2026
9c212ca
fix(envcanada): restore complete weatherType map (codes 0-48)
KristjanESPERANTO Feb 21, 2026
bba74cf
fix(pirateweather): use correct WeatherObject field names
KristjanESPERANTO Feb 21, 2026
07ca8fe
fix(weatherflow): compare full date instead of day-of-month
KristjanESPERANTO Feb 21, 2026
efc8892
refactor(tests): replace waitForTimeout with deterministic waits
KristjanESPERANTO Feb 21, 2026
e98cc1a
style(tests): break multi-statement line in ukmetoffice test
KristjanESPERANTO Feb 21, 2026
125ccd6
fix(envcanada): set temperature to null when unavailable
KristjanESPERANTO Feb 21, 2026
a1fbeca
fix(envcanada): handle empty currentConditions element
KristjanESPERANTO Feb 21, 2026
a03b0b2
revert: restore CORS proxy for newsfeed and 3rd-party module compatib…
KristjanESPERANTO Feb 21, 2026
be51f9c
tests(weather): fix EnvCanada provider to read current weather from c…
KristjanESPERANTO Feb 21, 2026
b09d6e5
refactor: use hasOwnProperty for precipAccumulation check
KristjanESPERANTO Feb 21, 2026
d87a946
feat(ukmetofficedatahub): add withFutureDailyTimes function to shift …
KristjanESPERANTO Feb 21, 2026
33b29c6
refactor(weather): change method visibility to private
KristjanESPERANTO Feb 21, 2026
91b5171
refactor(pirateweather): change default units from 'us' to 'si' in ge…
KristjanESPERANTO Feb 21, 2026
832bd04
feat(http_fetcher): improve network error handling with exponential b…
KristjanESPERANTO Feb 21, 2026
522ddf9
fix(weatherbit): fix weather icon mappings
KristjanESPERANTO Feb 21, 2026
0120dbc
feat(weatherapi): implement WeatherAPIProvider for current, daily, an…
KristjanESPERANTO Feb 21, 2026
e3dd77e
refactor(weather): extract cardinalToDegrees() to provider-utils.js
KristjanESPERANTO Feb 21, 2026
5df0776
feat(weatherapi): improve daily forecast wind direction accuracy
KristjanESPERANTO Feb 21, 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