mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-07-10 01:04:28 +00:00
## [2.27.0] - 2024-04-01 Thanks to: @bugsounet, @crazyscot, @illimarkangur, @jkriegshauser, @khassel, @KristjanESPERANTO, @Paranoid93, @rejas, @sdetweil and @vppencilsharpener. This release marks the first release without Michael Teeuw (@michmich). A very special thanks to him for creating MagicMirror and leading the project for so many years. For more info, please read the following post: [A New Chapter for MagicMirror: The Community Takes the Lead](https://forum.magicmirror.builders/topic/18329/a-new-chapter-for-magicmirror-the-community-takes-the-lead). ### Added - Output of system information to the console for troubleshooting (#3328 and #3337), ignore errors under aarch64 (#3349) - [chore] Add `eslint-plugin-package-json` to lint the `package.json` files (#3368) - [weather] `showHumidity` config is now a string describing where to show this element. Supported values: "wind", "temp", "feelslike", "below", "none". (#3330) - electron-rebuild test suite for electron and 3rd party modules compatibility (#3392) - Create MM² icon and attach it to electron process (#3407) ### Updated - Update updatenotification (update_helper.js): Recode with pm2 library (#3332) - Removing lodash dependency by replacing merge by spread operator (#3339) - Use node prefix for build-in modules (#3340) - Rework logging colors (#3350) - Update pm2 to v5.3.1 with no allow-ghsas (#3364) - [chore] Update husky and let lint-staged fix ESLint issues - [chore] Update dependencies including electron to v29 (#3357) and node-ical - Update translations for estonian (#3371) - Update electron to v29 and update other dependencies - [calendar] fullDay events over several days now show the left days from the first day on and 'today' on the last day - Update layout of current weather indoor values ### Fixed - Correct apibase of weathergov weatherprovider to match documentation (#2926) - Worked around several issues in the RRULE library that were causing deleted calender events to still show, some initial and recurring events to not show, and some event times to be off an hour. (#3291) - Skip changelog requirement when running tests for dependency updates (#3320) - Display precipitation probability when it is 0% instead of blank/empty (#3345) - [newsfeed] Suppress unsightly animation cases when there are 0 or 1 active news items (#3336) - [newsfeed] Always compute the feed item URL using the same helper function (#3336) - Ignore all custom css files (#3359) - [newsfeed] Fix newsfeed stall issue introduced by #3336 (#3361) - Changed `log.debug` to `log.log` in `app.js` where logLevel is not set because config is not loaded at this time (#3353) - [calendar] deny fetch interval < 60000 and set 60000 in this case (prevent fetch loop failed) (#3382) - added message in case where config.js is missing the module.export line PR #3383 - Fixed an issue where recurring events could extend past their recurrence end date (#3393) - Don't display any `npm WARN <....>` on install (#3399) - Fixed move suncalc dependency to production from dev, as it is used by clock module - [compliments] Fix mirror not responding anymore when no compliments are to be shown (#3385) ### Deleted - Unneeded file headers (#3358) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Michael Teeuw <michael@xonaymedia.nl> Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Karsten Hassel <hassel@gmx.de> Co-authored-by: Ross Younger <crazyscot@gmail.com> Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr> Co-authored-by: jkriegshauser <joshuakr@nvidia.com> Co-authored-by: illimarkangur <116028111+illimarkangur@users.noreply.github.com> Co-authored-by: sam detweiler <sdetweil@gmail.com> Co-authored-by: vppencilsharpener <tim.pray@gmail.com> Co-authored-by: Paranoid93 <6515818+Paranoid93@users.noreply.github.com>
371 lines
12 KiB
JavaScript
371 lines
12 KiB
JavaScript
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
|
|
|
/* Provider: weather.gov
|
|
* https://weather-gov.github.io/api/general-faqs
|
|
*
|
|
* This class is a provider for weather.gov.
|
|
* Note that this is only for US locations (lat and lon) and does not require an API key
|
|
* Since it is free, there are some items missing - like sunrise, sunset
|
|
*/
|
|
|
|
WeatherProvider.register("weathergov", {
|
|
// Set the name of the provider.
|
|
// This isn't strictly necessary, since it will fallback to the provider identifier
|
|
// But for debugging (and future alerts) it would be nice to have the real name.
|
|
providerName: "Weather.gov",
|
|
|
|
// Set the default config properties that is specific to this provider
|
|
defaults: {
|
|
apiBase: "https://api.weather.gov/points/",
|
|
lat: 0,
|
|
lon: 0
|
|
},
|
|
|
|
// Flag all needed URLs availability
|
|
configURLs: false,
|
|
|
|
//This API has multiple urls involved
|
|
forecastURL: "tbd",
|
|
forecastHourlyURL: "tbd",
|
|
forecastGridDataURL: "tbd",
|
|
observationStationsURL: "tbd",
|
|
stationObsURL: "tbd",
|
|
|
|
// Called to set the config, this config is the same as the weather module's config.
|
|
setConfig (config) {
|
|
this.config = config;
|
|
this.fetchWxGovURLs(this.config);
|
|
},
|
|
|
|
// Called when the weather provider is about to start.
|
|
start () {
|
|
Log.info(`Weather provider: ${this.providerName} started.`);
|
|
},
|
|
|
|
// This returns the name of the fetched location or an empty string.
|
|
fetchedLocation () {
|
|
return this.fetchedLocationName || "";
|
|
},
|
|
|
|
// Overwrite the fetchCurrentWeather method.
|
|
fetchCurrentWeather () {
|
|
if (!this.configURLs) {
|
|
Log.info("fetchCurrentWeather: fetch wx waiting on config URLs");
|
|
return;
|
|
}
|
|
this.fetchData(this.stationObsURL)
|
|
.then((data) => {
|
|
if (!data || !data.properties) {
|
|
// Did not receive usable new data.
|
|
return;
|
|
}
|
|
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties);
|
|
this.setCurrentWeather(currentWeather);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load station obs data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherForecast method.
|
|
fetchWeatherForecast () {
|
|
if (!this.configURLs) {
|
|
Log.info("fetchWeatherForecast: fetch wx waiting on config URLs");
|
|
return;
|
|
}
|
|
this.fetchData(this.forecastURL)
|
|
.then((data) => {
|
|
if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) {
|
|
// Did not receive usable new data.
|
|
return;
|
|
}
|
|
const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods);
|
|
this.setWeatherForecast(forecast);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load forecast hourly data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
// Overwrite the fetchWeatherHourly method.
|
|
fetchWeatherHourly () {
|
|
if (!this.configURLs) {
|
|
Log.info("fetchWeatherHourly: fetch wx waiting on config URLs");
|
|
return;
|
|
}
|
|
this.fetchData(this.forecastHourlyURL)
|
|
.then((data) => {
|
|
if (!data) {
|
|
// Did not receive usable new data.
|
|
// Maybe this needs a better check?
|
|
return;
|
|
}
|
|
const hourly = this.generateWeatherObjectsFromHourly(data.properties.periods);
|
|
this.setWeatherHourly(hourly);
|
|
})
|
|
.catch(function (request) {
|
|
Log.error("Could not load data ... ", request);
|
|
})
|
|
.finally(() => this.updateAvailable());
|
|
},
|
|
|
|
/** Weather.gov Specific Methods - These are not part of the default provider methods */
|
|
|
|
/*
|
|
* Get specific URLs
|
|
*/
|
|
fetchWxGovURLs (config) {
|
|
this.fetchData(`${config.apiBase}/${config.lat},${config.lon}`)
|
|
.then((data) => {
|
|
if (!data || !data.properties) {
|
|
// points URL did not respond with usable data.
|
|
return;
|
|
}
|
|
this.fetchedLocationName = `${data.properties.relativeLocation.properties.city}, ${data.properties.relativeLocation.properties.state}`;
|
|
Log.log(`Forecast location is ${this.fetchedLocationName}`);
|
|
this.forecastURL = `${data.properties.forecast}?units=si`;
|
|
this.forecastHourlyURL = `${data.properties.forecastHourly}?units=si`;
|
|
this.forecastGridDataURL = data.properties.forecastGridData;
|
|
this.observationStationsURL = data.properties.observationStations;
|
|
// with this URL, we chain another promise for the station obs URL
|
|
return this.fetchData(data.properties.observationStations);
|
|
})
|
|
.then((obsData) => {
|
|
if (!obsData || !obsData.features) {
|
|
// obs station URL did not respond with usable data.
|
|
return;
|
|
}
|
|
this.stationObsURL = `${obsData.features[0].id}/observations/latest`;
|
|
})
|
|
.catch((err) => {
|
|
Log.error(err);
|
|
})
|
|
.finally(() => {
|
|
// excellent, let's fetch some actual wx data
|
|
this.configURLs = true;
|
|
|
|
// handle 'forecast' config, fall back to 'current'
|
|
if (config.type === "forecast") {
|
|
this.fetchWeatherForecast();
|
|
} else if (config.type === "hourly") {
|
|
this.fetchWeatherHourly();
|
|
} else {
|
|
this.fetchCurrentWeather();
|
|
}
|
|
});
|
|
},
|
|
|
|
/*
|
|
* Generate a WeatherObject based on hourlyWeatherInformation
|
|
* Weather.gov API uses specific units; API does not include choice of units
|
|
* ... object needs data in units based on config!
|
|
*/
|
|
generateWeatherObjectsFromHourly (forecasts) {
|
|
const days = [];
|
|
|
|
// variable for date
|
|
let weather = new WeatherObject();
|
|
for (const forecast of forecasts) {
|
|
weather.date = moment(forecast.startTime.slice(0, 19));
|
|
if (forecast.windSpeed.search(" ") < 0) {
|
|
weather.windSpeed = forecast.windSpeed;
|
|
} else {
|
|
weather.windSpeed = forecast.windSpeed.slice(0, forecast.windSpeed.search(" "));
|
|
}
|
|
weather.windSpeed = WeatherUtils.convertWindToMs(weather.windSpeed);
|
|
weather.windFromDirection = forecast.windDirection;
|
|
weather.temperature = forecast.temperature;
|
|
//assign probability of precipitation
|
|
if (forecast.probabilityOfPrecipitation.value === null) {
|
|
weather.precipitationProbability = 0;
|
|
} else {
|
|
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
|
|
}
|
|
// use the forecast isDayTime attribute to help build the weatherType label
|
|
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
|
|
|
days.push(weather);
|
|
|
|
weather = new WeatherObject();
|
|
}
|
|
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
return days;
|
|
},
|
|
|
|
/*
|
|
* Generate a WeatherObject based on currentWeatherInformation
|
|
* Weather.gov API uses specific units; API does not include choice of units
|
|
* ... object needs data in units based on config!
|
|
*/
|
|
generateWeatherObjectFromCurrentWeather (currentWeatherData) {
|
|
const currentWeather = new WeatherObject();
|
|
|
|
currentWeather.date = moment(currentWeatherData.timestamp);
|
|
currentWeather.temperature = currentWeatherData.temperature.value;
|
|
currentWeather.windSpeed = WeatherUtils.convertWindToMs(currentWeatherData.windSpeed.value);
|
|
currentWeather.windFromDirection = currentWeatherData.windDirection.value;
|
|
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
|
|
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
|
|
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
|
|
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value;
|
|
if (currentWeatherData.heatIndex.value !== null) {
|
|
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
|
|
} else if (currentWeatherData.windChill.value !== null) {
|
|
currentWeather.feelsLikeTemp = currentWeatherData.windChill.value;
|
|
} else {
|
|
currentWeather.feelsLikeTemp = currentWeatherData.temperature.value;
|
|
}
|
|
// determine the sunrise/sunset times - not supplied in weather.gov data
|
|
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
|
|
|
// update weatherType
|
|
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, currentWeather.isDayTime());
|
|
|
|
return currentWeather;
|
|
},
|
|
|
|
/*
|
|
* Generate WeatherObjects based on forecast information
|
|
*/
|
|
generateWeatherObjectsFromForecast (forecasts) {
|
|
return this.fetchForecastDaily(forecasts);
|
|
},
|
|
|
|
/*
|
|
* fetch forecast information for daily forecast.
|
|
*/
|
|
fetchForecastDaily (forecasts) {
|
|
// initial variable declaration
|
|
const days = [];
|
|
// variables for temperature range and rain
|
|
let minTemp = [];
|
|
let maxTemp = [];
|
|
// variable for date
|
|
let date = "";
|
|
let weather = new WeatherObject();
|
|
|
|
for (const forecast of forecasts) {
|
|
if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) {
|
|
// calculate minimum/maximum temperature, specify rain amount
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
// create new weather-object
|
|
weather = new WeatherObject();
|
|
|
|
minTemp = [];
|
|
maxTemp = [];
|
|
//assign probability of precipitation
|
|
if (forecast.probabilityOfPrecipitation.value === null) {
|
|
weather.precipitationProbability = 0;
|
|
} else {
|
|
weather.precipitationProbability = forecast.probabilityOfPrecipitation.value;
|
|
}
|
|
|
|
// set new date
|
|
date = moment(forecast.startTime).format("YYYY-MM-DD");
|
|
|
|
// specify date
|
|
weather.date = moment(forecast.startTime);
|
|
|
|
// use the forecast isDayTime attribute to help build the weatherType label
|
|
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
|
}
|
|
|
|
if (moment(forecast.startTime).format("H") >= 8 && moment(forecast.startTime).format("H") <= 17) {
|
|
weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime);
|
|
}
|
|
|
|
// the same day as before
|
|
// add values from forecast to corresponding variables
|
|
minTemp.push(forecast.temperature);
|
|
maxTemp.push(forecast.temperature);
|
|
}
|
|
|
|
// last day
|
|
// calculate minimum/maximum temperature
|
|
weather.minTemperature = Math.min.apply(null, minTemp);
|
|
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
|
|
|
// push weather information to days array
|
|
days.push(weather);
|
|
return days.slice(1);
|
|
},
|
|
|
|
/*
|
|
* Convert the icons to a more usable name.
|
|
*/
|
|
convertWeatherType (weatherType, isDaytime) {
|
|
//https://w1.weather.gov/xml/current_obs/weather.php
|
|
// There are way too many types to create, so lets just look for certain strings
|
|
|
|
if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) {
|
|
if (isDaytime) {
|
|
return "day-cloudy";
|
|
}
|
|
|
|
return "night-cloudy";
|
|
} else if (weatherType.includes("Overcast")) {
|
|
if (isDaytime) {
|
|
return "cloudy";
|
|
}
|
|
|
|
return "night-cloudy";
|
|
} else if (weatherType.includes("Freezing") || weatherType.includes("Ice")) {
|
|
return "rain-mix";
|
|
} else if (weatherType.includes("Snow")) {
|
|
if (isDaytime) {
|
|
return "snow";
|
|
}
|
|
|
|
return "night-snow";
|
|
} else if (weatherType.includes("Thunderstorm")) {
|
|
if (isDaytime) {
|
|
return "thunderstorm";
|
|
}
|
|
|
|
return "night-thunderstorm";
|
|
} else if (weatherType.includes("Showers")) {
|
|
if (isDaytime) {
|
|
return "showers";
|
|
}
|
|
|
|
return "night-showers";
|
|
} else if (weatherType.includes("Rain") || weatherType.includes("Drizzle")) {
|
|
if (isDaytime) {
|
|
return "rain";
|
|
}
|
|
|
|
return "night-rain";
|
|
} else if (weatherType.includes("Breezy") || weatherType.includes("Windy")) {
|
|
if (isDaytime) {
|
|
return "cloudy-windy";
|
|
}
|
|
|
|
return "night-alt-cloudy-windy";
|
|
} else if (weatherType.includes("Fair") || weatherType.includes("Clear") || weatherType.includes("Few") || weatherType.includes("Sunny")) {
|
|
if (isDaytime) {
|
|
return "day-sunny";
|
|
}
|
|
|
|
return "night-clear";
|
|
} else if (weatherType.includes("Dust") || weatherType.includes("Sand")) {
|
|
return "dust";
|
|
} else if (weatherType.includes("Fog")) {
|
|
return "fog";
|
|
} else if (weatherType.includes("Smoke")) {
|
|
return "smoke";
|
|
} else if (weatherType.includes("Haze")) {
|
|
return "day-haze";
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|