Files
Veeck 4b4a59534f Release 2.37.0 (#4194)
## Release Notes
Thanks to: @angeldeejay, @egeekial, @khassel, @KristjanESPERANTO,
@MikeBishop, @rejas
> ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change
to previous release)

[Compare to previous Release
v2.36.0](https://github.com/MagicMirrorOrg/MagicMirror/compare/v2.36.0...develop)


### [core]
- Prepare Release 2.37.0 (#4193)
- fix(electron): map IPv6 :: wildcard to localhost (#4188)
- refactor(main): modernize DOM update flow with async/await (#4186)
- refactor(main): simplify _updateDom with async/await (#4185)
- fix(security): prevent unauthorized secret expansion in socket
payloads (#4184)
- refactor(main): simplify updateDomWithContent async flow (#4182)
- fix: modules losing data after HTTP 304 responses (#4180)
- chore: add missing core defaults (#4181)
- fix(server): enforce ipWhitelist for Socket.IO too (#4169)
- feat(systeminfo): include Git hash and branch in system information
log (#4167)
- feat(electron): support object-based electronSwitches (#4161)
- systeminformation thread not ending: move error handling from utils to
app (#4160)
- fix systeminformation thread not ending (#4155)
- refactor: use ES module imports in browser core (#4158)
- refactor(core): remove old Object.assign polyfill (#4157)
- refactor: rewrite Module as an ES6 class (#4151)
- refactor: rewrite NodeHelper as an ES6 class (#4147)
- update eletron to v42 (#4144)
- refactor(utils): drop ajv dependency (#4142)
- fix(systeminformation): output right 'used node' version (from parent
process) (#4141)
- fix: skip postinstall git clean when not in a git repository (#4139)
- Remove unnecessary conditionals and fix falsy property check in
imperial conversion (#4135)
- update version in package.json

### [dependencies]
- update dependencies (#4191)
- Bump actions/checkout from 6 to 7 (#4190)
- chore: update dependencies and adjust import path for SunCalc (#4189)
- update dependencies incl. electron and revert
yauzl-electron-install-fix (#4183)
- update dependencies, add electron fix in package.json (#4175)
- chore: update dependencies (#4162)
- Bump actions/dependency-review-action from 4 to 5 (#4152)
- Unify linting: replace Stylelint and markdownlint with ESLint (#4148)
- update dependencies and workflows to node v26 (#4140)

### [modules/alert]
- CodeQL cleanup for alerts #18, #19, #20 (#4153)
- fix: resolve CodeQL alerts #24 and #26 (#4145)
- fix(electron): resolve CodeQL alerts #22 and #25 in electron.js
(#4136)

### [modules/calendar]
- perf(calendar): pre-filter ICS data before parsing (#4168)
- perf(calendar): use async ICS parsing to avoid blocking event loop
(#4143)

### [modules/newsfeed]
- [newsfeed] add allowBasicHtmlTags option for basic emphasis (#4176)

### [modules/updatenotification]
- fix(updatenotification): don't spawn a child process when running
under PM2 (#4166)
- fix(updatenotification): use process.argv[0] as restart binary (#4163)
- fix(updatenotification): preserve start mode on restart (#4156)
- fix(updatenotification): fix ref diff parsing for fetch --dry-run
(#4138)
- refactor(updatenotification): replace pm2 usage with node logic
(#4134)

### [modules/weather]
- feat(weather): add Buienradar provider (#4164)

### [testing]
- remove warning in unit tests (for nodejs >= v25) (#4149)
- polish HTTP 304 docs/test/handling (#4129)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com>
Co-authored-by: Karsten Hassel <hassel@gmx.de>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Koen Konst <koenspero@gmail.com>
Co-authored-by: Koen Konst <c.h.konst@avisi.nl>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: dathbe <github@beffa.us>
Co-authored-by: veeck <gitkraken@veeck.de>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Marcel <m-idler@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: Kevin G. <crazylegstoo@gmail.com>
Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com>
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com>
Co-authored-by: Jordan Welch <JordanHWelch@gmail.com>
Co-authored-by: Blackspirits <blackspirits@gmail.com>
Co-authored-by: Samed Ozdemir <samed@xsor.io>
Co-authored-by: in-voker <58696565+in-voker@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
Co-authored-by: cgillinger <christian.gillinger@gmail.com>
Co-authored-by: Sonny B <43247590+sonnyb9@users.noreply.github.com>
Co-authored-by: sonnyb9 <sonnyb9@users.noreply.github.com>
Co-authored-by: Morgan McBee <egeekial@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Mike Bishop <mbishop@evequefou.be>
2026-07-01 22:01:42 +02:00

303 lines
8.4 KiB
JavaScript

const Log = require("logger");
const { convertKmhToMs } = require("../provider-utils");
const HTTPFetcher = require("#http_fetcher");
/**
* WeatherFlow weather provider
* This class is a provider for WeatherFlow personal weather stations.
* Note that the WeatherFlow API does not provide snowfall.
*/
class WeatherFlowProvider {
/**
* @param {object} config - Provider configuration
*/
constructor (config) {
this.config = config;
this.fetcher = null;
this.onDataCallback = null;
this.onErrorCallback = null;
}
/**
* Set the callbacks for data and errors
* @param {(data: object) => void} onDataCallback - Called when new data is available
* @param {(error: object) => void} onErrorCallback - Called when an error occurs
*/
setCallbacks (onDataCallback, onErrorCallback) {
this.onDataCallback = onDataCallback;
this.onErrorCallback = onErrorCallback;
}
/**
* Initialize the provider
*/
initialize () {
if (!this.config.token || this.config.token === "YOUR_API_TOKEN_HERE") {
Log.error("[weatherflow] No API token configured. Get one at https://tempestwx.com/");
if (this.onErrorCallback) {
this.onErrorCallback({
message: "WeatherFlow API token required. Get one at https://tempestwx.com/",
translationKey: "MODULE_ERROR_UNSPECIFIED"
});
}
return;
}
if (!this.config.stationid) {
Log.error("[weatherflow] No station ID configured");
if (this.onErrorCallback) {
this.onErrorCallback({
message: "WeatherFlow station ID required",
translationKey: "MODULE_ERROR_UNSPECIFIED"
});
}
return;
}
this.#initializeFetcher();
}
/**
* Initialize the HTTP fetcher
*/
#initializeFetcher () {
const url = this.#getUrl();
this.fetcher = new HTTPFetcher(url, {
reloadInterval: this.config.updateInterval,
headers: {
"Cache-Control": "no-cache",
Accept: "application/json"
},
logContext: "weatherprovider.weatherflow"
});
this.fetcher.on("response", async (response) => {
if (response.status === 304) return;
try {
const data = await response.json();
const processed = this.#processData(data);
this.onDataCallback(processed);
} catch (error) {
Log.error("[weatherflow] Failed to parse JSON:", error);
}
});
this.fetcher.on("error", (errorInfo) => {
// HTTPFetcher already logged the error with logContext
if (this.onErrorCallback) {
this.onErrorCallback(errorInfo);
}
});
}
/**
* Generate the URL for API requests
* @returns {string} The API URL
*/
#getUrl () {
const base = this.config.apiBase || "https://swd.weatherflow.com/swd/rest/";
return `${base}better_forecast?station_id=${this.config.stationid}&units_temp=c&units_wind=kph&units_pressure=mb&units_precip=mm&units_distance=km&token=${this.config.token}`;
}
/**
* Process the raw API data
* @param {object} data - Raw API response
* @returns {object} Processed weather data
*/
#processData (data) {
try {
let weatherData;
if (this.config.type === "current") {
weatherData = this.#generateCurrent(data);
} else if (this.config.type === "hourly") {
weatherData = this.#generateHourly(data);
} else {
weatherData = this.#generateDaily(data);
}
return weatherData;
} catch (error) {
Log.error("[weatherflow] Data processing error:", error);
if (this.onErrorCallback) {
this.onErrorCallback({
message: "Failed to process weather data",
translationKey: "MODULE_ERROR_UNSPECIFIED"
});
}
return null;
}
}
/**
* Generate current weather data
* @param {object} data - API response data
* @returns {object} Current weather object
*/
#generateCurrent (data) {
if (!data || !data.current_conditions || !data.forecast || !Array.isArray(data.forecast.daily) || data.forecast.daily.length === 0) {
Log.error("[weatherflow] Invalid current weather data structure");
return null;
}
const current = data.current_conditions;
const daily = data.forecast.daily[0];
const weather = {
date: new Date(),
humidity: current.relative_humidity ?? null,
temperature: current.air_temperature ?? null,
feelsLikeTemp: current.feels_like ?? null,
windSpeed: current.wind_avg != null ? convertKmhToMs(current.wind_avg) : null,
windFromDirection: current.wind_direction ?? null,
weatherType: this.#convertWeatherType(current.icon),
precipitationAmount: current.precip_accum_local_day ?? null,
precipitationUnits: "mm",
precipitationProbability: current.precip_probability ?? null,
uvIndex: current.uv || null,
sunrise: daily.sunrise ? new Date(daily.sunrise * 1000) : null,
sunset: daily.sunset ? new Date(daily.sunset * 1000) : null
};
return weather;
}
/**
* Generate forecast data
* @param {object} data - API response data
* @returns {Array} Array of forecast objects
*/
#generateDaily (data) {
if (!data || !data.forecast || !Array.isArray(data.forecast.daily) || !Array.isArray(data.forecast.hourly)) {
Log.error("[weatherflow] Invalid forecast data structure");
return [];
}
const days = [];
for (const forecast of data.forecast.daily) {
const weather = {
date: new Date(forecast.day_start_local * 1000),
minTemperature: forecast.air_temp_low ?? null,
maxTemperature: forecast.air_temp_high ?? null,
precipitationProbability: forecast.precip_probability ?? null,
weatherType: this.#convertWeatherType(forecast.icon),
precipitationAmount: 0.0,
precipitationUnits: "mm",
uvIndex: 0
};
// Build UV and precipitation from hourly data
for (const hour of data.forecast.hourly) {
const hourDate = new Date(hour.time * 1000);
const forecastDate = new Date(forecast.day_start_local * 1000);
// Compare year, month, and day to ensure correct matching across month boundaries
if (hourDate.getFullYear() === forecastDate.getFullYear()
&& hourDate.getMonth() === forecastDate.getMonth()
&& hourDate.getDate() === forecastDate.getDate()) {
weather.uvIndex = Math.max(weather.uvIndex, hour.uv ?? 0);
weather.precipitationAmount += hour.precip ?? 0;
} else if (hourDate > forecastDate) {
// Check if we've moved to the next day
const diffMs = hourDate - forecastDate;
if (diffMs >= 86400000) break; // 24 hours in ms
}
}
days.push(weather);
}
return days;
}
/**
* Generate hourly forecast data
* @param {object} data - API response data
* @returns {Array} Array of hourly forecast objects
*/
#generateHourly (data) {
if (!data || !data.forecast || !Array.isArray(data.forecast.hourly)) {
Log.error("[weatherflow] Invalid hourly data structure");
return [];
}
const hours = [];
for (const hour of data.forecast.hourly) {
const weather = {
date: new Date(hour.time * 1000),
temperature: hour.air_temperature ?? null,
feelsLikeTemp: hour.feels_like ?? null,
humidity: hour.relative_humidity ?? null,
windSpeed: hour.wind_avg != null ? convertKmhToMs(hour.wind_avg) : null,
windFromDirection: hour.wind_direction ?? null,
weatherType: this.#convertWeatherType(hour.icon),
precipitationProbability: hour.precip_probability ?? null,
precipitationAmount: hour.precip ?? 0,
precipitationUnits: "mm",
uvIndex: hour.uv || null
};
hours.push(weather);
// WeatherFlow provides 10 days of hourly data, trim to 48 hours
if (hours.length >= 48) break;
}
return hours;
}
/**
* Convert weather icon type
* @param {string} weatherType - WeatherFlow icon code
* @returns {string} Weather icon CSS class
*/
#convertWeatherType (weatherType) {
const weatherTypes = {
"clear-day": "day-sunny",
"clear-night": "night-clear",
cloudy: "cloudy",
foggy: "fog",
"partly-cloudy-day": "day-cloudy",
"partly-cloudy-night": "night-alt-cloudy",
"possibly-rainy-day": "day-rain",
"possibly-rainy-night": "night-alt-rain",
"possibly-sleet-day": "day-sleet",
"possibly-sleet-night": "night-alt-sleet",
"possibly-snow-day": "day-snow",
"possibly-snow-night": "night-alt-snow",
"possibly-thunderstorm-day": "day-thunderstorm",
"possibly-thunderstorm-night": "night-alt-thunderstorm",
rainy: "rain",
sleet: "sleet",
snow: "snow",
thunderstorm: "thunderstorm",
windy: "strong-wind"
};
return weatherTypes[weatherType] || null;
}
/**
* Start fetching data
*/
start () {
if (this.fetcher) {
this.fetcher.startPeriodicFetch();
}
}
/**
* Stop fetching data
*/
stop () {
if (this.fetcher) {
this.fetcher.clearTimer();
}
}
}
module.exports = WeatherFlowProvider;