mirror of
https://github.com/MichMich/MagicMirror.git
synced 2026-04-23 14:27:01 +00:00
This migrates the Weather module from client-side fetching to use the server-side centralized HTTPFetcher (introduced in #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 - **Security**: Possibility to hide API keys/secrets from client-side - **Performance**: Reduced API calls in multi-client setups - one server fetch instead of one per client - **Enabling possible future features**: e.g. server-side caching, rate limit monitoring, and data sharing with third-party modules ## 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 this 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. ## Review Notes I know this is an enormous change - I've been working on this for quite some time. Unfortunately, breaking it into smaller incremental PRs wasn't feasible due to the interdependencies between providers and the shared architecture. Given the scope, it's nearly impossible to manually review every change. To ensure quality, I've used both CodeRabbit and GitHub Copilot to review the code multiple times in my fork, and both provided extensive and valuable feedback. Most importantly, my test setup with all 10 providers working successfully is very encouraging. ## Related Part of the HTTPFetcher migration #4016. ## Screenshots <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-06-54" src="https://github.com/user-attachments/assets/2139f4d2-2a9b-4e49-8d0a-e4436983ed6e" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-02" src="https://github.com/user-attachments/assets/880f7ce2-4e44-42d5-bfe4-5ce475cca7c2" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-07" src="https://github.com/user-attachments/assets/abd89933-fe03-40ab-8a7c-41ae1ff99255" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-12" src="https://github.com/user-attachments/assets/22225852-f0a9-4d33-87ab-0733ba30fad3" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-17" src="https://github.com/user-attachments/assets/7a7192a5-f237-4060-85d7-6f50b9bef5af" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-22" src="https://github.com/user-attachments/assets/df84d9f1-e531-4995-8da8-d6f2601b6a08" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-27" src="https://github.com/user-attachments/assets/4cf391ac-db43-4b52-95f4-f5eadc5ea34d" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-32" src="https://github.com/user-attachments/assets/8dd8e688-d47f-4815-87f6-7f2630f15d58" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-37" src="https://github.com/user-attachments/assets/ee84a8bc-6b35-405a-b311-88658d9268dd" /> <img width="1920" height="1080" alt="Ekrankopio de 2026-02-08 13-07-42" src="https://github.com/user-attachments/assets/f941f341-453f-4d4d-a8d9-6b9158eb2681" /> Provider "Weather API" added later: <img width="1910" height="1080" alt="Ekrankopio de 2026-02-15 19-39-06" src="https://github.com/user-attachments/assets/3f0c8ba3-105c-4f90-8b2e-3a1be543d3d2" />
206 lines
6.9 KiB
JavaScript
206 lines
6.9 KiB
JavaScript
const WeatherUtils = {
|
|
|
|
/**
|
|
* Convert wind (from m/s) to beaufort scale
|
|
* @param {number} speedInMS the windspeed you want to convert
|
|
* @returns {number} the speed in beaufort
|
|
*/
|
|
beaufortWindSpeed (speedInMS) {
|
|
const windInKmh = this.convertWind(speedInMS, "kmh");
|
|
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
|
|
for (const [index, speed] of speeds.entries()) {
|
|
if (speed > windInKmh) {
|
|
return index;
|
|
}
|
|
}
|
|
return 12;
|
|
},
|
|
|
|
/**
|
|
* Convert a value in a given unit to a string with a converted
|
|
* value and a postfix matching the output unit system.
|
|
* @param {number} value - The value to convert.
|
|
* @param {string} valueUnit - The unit the values has. Default is mm.
|
|
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
|
|
* @returns {string} - A string with tha value and a unit postfix.
|
|
*/
|
|
convertPrecipitationUnit (value, valueUnit, outputUnit) {
|
|
if (value === null || value === undefined || isNaN(value)) {
|
|
return "";
|
|
}
|
|
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
|
|
|
|
let convertedValue = value;
|
|
let conversionUnit = valueUnit;
|
|
if (outputUnit === "imperial") {
|
|
convertedValue = this.convertPrecipitationToInch(value, valueUnit);
|
|
conversionUnit = "in";
|
|
} else {
|
|
conversionUnit = valueUnit ? valueUnit : "mm";
|
|
}
|
|
|
|
return `${convertedValue.toFixed(2)} ${conversionUnit}`;
|
|
},
|
|
|
|
/**
|
|
* Convert precipitation value into inch
|
|
* @param {number} value the precipitation value for convert
|
|
* @param {string} valueUnit can be 'mm' or 'cm'
|
|
* @returns {number} the converted precipitation value
|
|
*/
|
|
convertPrecipitationToInch (value, valueUnit) {
|
|
if (valueUnit && valueUnit.toLowerCase() === "cm") return value * 0.3937007874;
|
|
else return value * 0.03937007874;
|
|
},
|
|
|
|
/**
|
|
* Convert temp (from degrees C) into imperial or metric unit depending on
|
|
* your config
|
|
* @param {number} tempInC the temperature in Celsius you want to convert
|
|
* @param {string} unit can be 'imperial' or 'metric'
|
|
* @returns {number} the converted temperature
|
|
*/
|
|
convertTemp (tempInC, unit) {
|
|
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
|
|
},
|
|
|
|
/**
|
|
* Convert temp (from degrees C) into metric unit
|
|
* @param {number} tempInF the temperature in Fahrenheit you want to convert
|
|
* @returns {number} the converted temperature
|
|
*/
|
|
convertTempToMetric (tempInF) {
|
|
return ((tempInF - 32) * 5) / 9;
|
|
},
|
|
|
|
/**
|
|
* Convert wind speed into another unit.
|
|
* @param {number} windInMS the windspeed in meter/sec you want to convert
|
|
* @param {string} unit can be 'beaufort', 'kmh', 'knots, 'imperial' (mph)
|
|
* or 'metric' (mps)
|
|
* @returns {number} the converted windspeed
|
|
*/
|
|
convertWind (windInMS, unit) {
|
|
switch (unit) {
|
|
case "beaufort":
|
|
return this.beaufortWindSpeed(windInMS);
|
|
case "kmh":
|
|
return (windInMS * 3600) / 1000;
|
|
case "knots":
|
|
return windInMS * 1.943844;
|
|
case "imperial":
|
|
return windInMS * 2.2369362920544;
|
|
case "metric":
|
|
default:
|
|
return windInMS;
|
|
}
|
|
},
|
|
|
|
/*
|
|
* Convert the wind direction cardinal to value
|
|
*/
|
|
convertWindDirection (windDirection) {
|
|
const windCardinals = {
|
|
N: 0,
|
|
NNE: 22,
|
|
NE: 45,
|
|
ENE: 67,
|
|
E: 90,
|
|
ESE: 112,
|
|
SE: 135,
|
|
SSE: 157,
|
|
S: 180,
|
|
SSW: 202,
|
|
SW: 225,
|
|
WSW: 247,
|
|
W: 270,
|
|
WNW: 292,
|
|
NW: 315,
|
|
NNW: 337
|
|
};
|
|
|
|
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
|
|
},
|
|
|
|
convertWindToMetric (mph) {
|
|
return mph / 2.2369362920544;
|
|
},
|
|
|
|
convertWindToMs (kmh) {
|
|
return kmh * 0.27777777777778;
|
|
},
|
|
|
|
/**
|
|
* Taken from https://community.home-assistant.io/t/calculating-apparent-feels-like-temperature/370834/18
|
|
* @param {number} temperature temperature in degrees Celsius
|
|
* @param {number} windSpeed wind speed in meter/second
|
|
* @param {number} humidity relative humidity in percent
|
|
* @returns {number} the feels like temperature in degrees Celsius
|
|
*/
|
|
calculateFeelsLike (temperature, windSpeed, humidity) {
|
|
const windInMph = this.convertWind(windSpeed, "imperial");
|
|
const tempInF = this.convertTemp(temperature, "imperial");
|
|
|
|
let HI;
|
|
let WC = tempInF;
|
|
|
|
// Calculate wind chill for certain conditions
|
|
if (tempInF <= 70 && windInMph >= 3) {
|
|
WC = 35.74 + (0.6215 * tempInF) - 35.75 * Math.pow(windInMph, 0.16) + ((0.4275 * tempInF) * Math.pow(windInMph, 0.16));
|
|
}
|
|
|
|
// Steadman Heat Index Vorberechnung
|
|
const STEADMAN_HI = 0.5 * (tempInF + 61.0 + ((tempInF - 68.0) * 1.2) + (humidity * 0.094));
|
|
|
|
if (STEADMAN_HI >= 80) {
|
|
// Rothfusz-Komplex
|
|
const ROTHFUSZ_HI = -42.379 + 2.04901523 * tempInF + 10.14333127 * humidity - 0.22475541 * tempInF * humidity - 0.00683783 * tempInF * tempInF - 0.05481717 * humidity * humidity + 0.00122874 * tempInF * tempInF * humidity + 0.00085282 * tempInF * humidity * humidity - 0.00000199 * tempInF * tempInF * humidity * humidity;
|
|
|
|
HI = ROTHFUSZ_HI;
|
|
|
|
if (humidity < 13 && tempInF > 80 && tempInF < 112) {
|
|
const ADJUSTMENT = ((13 - humidity) / 4) * Math.pow(Math.abs(17 - (tempInF - 95)), 0.5) / 17; // sqrt Teil
|
|
HI = HI - ADJUSTMENT;
|
|
} else if (humidity > 85 && tempInF > 80 && tempInF < 87) {
|
|
const ADJUSTMENT = ((humidity - 85) / 10) * ((87 - tempInF) / 5);
|
|
HI = HI + ADJUSTMENT;
|
|
}
|
|
|
|
} else { HI = STEADMAN_HI; }
|
|
|
|
// Feuchte Lastberechnung FL
|
|
let FL;
|
|
if (tempInF < 50) { FL = WC; }
|
|
else if (tempInF >= 50 && tempInF < 70) { FL = ((70 - tempInF) / 20) * WC + ((tempInF - 50) / 20) * HI; }
|
|
else if (tempInF >= 70) { FL = HI; }
|
|
|
|
return this.convertTempToMetric(FL);
|
|
},
|
|
|
|
/**
|
|
* Converts the Weather Object's values into imperial unit
|
|
* @param {object} weatherObject the weather object
|
|
* @returns {object} the weather object with converted values to imperial
|
|
*/
|
|
convertWeatherObjectToImperial (weatherObject) {
|
|
if (!weatherObject || Object.keys(weatherObject).length === 0) return null;
|
|
|
|
let imperialWeatherObject = { ...weatherObject };
|
|
|
|
if (imperialWeatherObject) {
|
|
if (imperialWeatherObject.feelsLikeTemp) imperialWeatherObject.feelsLikeTemp = this.convertTemp(imperialWeatherObject.feelsLikeTemp, "imperial");
|
|
if (imperialWeatherObject.maxTemperature) imperialWeatherObject.maxTemperature = this.convertTemp(imperialWeatherObject.maxTemperature, "imperial");
|
|
if (imperialWeatherObject.minTemperature) imperialWeatherObject.minTemperature = this.convertTemp(imperialWeatherObject.minTemperature, "imperial");
|
|
if (imperialWeatherObject.precipitationAmount) imperialWeatherObject.precipitationAmount = this.convertPrecipitationToInch(imperialWeatherObject.precipitationAmount, imperialWeatherObject.precipitationUnits);
|
|
if (imperialWeatherObject.temperature) imperialWeatherObject.temperature = this.convertTemp(imperialWeatherObject.temperature, "imperial");
|
|
if (imperialWeatherObject.windSpeed) imperialWeatherObject.windSpeed = this.convertWind(imperialWeatherObject.windSpeed, "imperial");
|
|
}
|
|
|
|
return imperialWeatherObject;
|
|
}
|
|
};
|
|
|
|
if (typeof module !== "undefined") {
|
|
module.exports = WeatherUtils;
|
|
}
|