mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-07-07 16:11:14 +00:00
## [2.23.0] - 2023-04-04 Thanks to: @angeldeejay, @buxxi, @CarJem, @dariom, @DaveChild, @dWoolridge, @grenagit, @Hirschberger, @KristjanESPERANTO, @MagMar94, @naveensrinivasan, @nfogal, @psieg, @rajniszp, @retroflex, @SkySails and @tomzt. Special thanks to @khassel, @rejas and @sdetweil for taking over most (if not all) of the work on this release as project collaborators. This version would not be there without their effort. Thank you guys! You are awesome! ### Added - Added increments for hourly forecasts in weather module (#2996) - Added tests for hourly weather forecast - Added possibility to ignore MagicMirror repo in updatenotification module - Added Pirate Weather as new weather provider (#3005) - Added possibility to use your own templates in Alert module - Added error message if `<modulename>.js` file is missing in module folder to get a hint in the logs (#2403) - Added possibility to use environment variables in `config.js` (#1756) - Added option `pastDaysCount` to default calendar module to control of how many days past events should be displayed - Added thai language to alert module - Added option `sendNotifications` in clock module (#3056) ### Removed - Removed darksky weather provider - Removed unneeded (and unwanted) '.' after the year in calendar repeatingCountTitle (#2896) ### Updated - Use develop as target branch for dependabot - Update issue template, contributing doc and sample config - The weather modules clearly separates precipitation amount and probability (risk of rain/snow) - This requires all providers that only supports probability to change the config from `showPrecipitationAmount` to `showPrecipitationProbability`. - Update tests for weather and calendar module - Changed updatenotification module for MagicMirror repo only: Send only notifications for `master` if there is a tag on a newer commit - Update dates in Calendar widgets every minute - Cleanup jest coverage for patches - Update `stylelint` dependencies, switch to `stylelint-config-standard` and handle `stylelint` issues, update `main.css` matching new rules - Update Eslint config, add new rule and handle issue - Convert lots of callbacks to async/await - Revise require imports (#3071 and #3072) ### Fixed - Fix wrong day labels in envcanada forecast (#2987) - Fix for missing default class name prefix for customEvents in calendar - Fix electron flashing white screen on startup (#1919) - Fix weathergov provider hourly forecast (#3008) - Fix message display with HTML code into alert module (#2828) - Fix typo in french translation - Yr wind direction is no longer inverted - Fix async node_helper stopping electron start (#2487) - The wind direction arrow now points in the direction the wind is flowing, not into the wind (#3019) - Fix precipitation css styles and rounding value - Fix wrong vertical alignment of calendar title column when wrapEvents is true (#3053) - Fix empty news feed stopping the reload forever - Fix e2e tests (failed after async changes) by running calendar and newsfeed tests last - Lint: Use template literals instead of string concatenation - Fix default alert module to render HTML for title and message - Fix Open-Meteo wind speed units
368 lines
12 KiB
JavaScript
368 lines
12 KiB
JavaScript
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
|
|
|
/* MagicMirror²
|
|
* Module: Weather
|
|
* Provider: weather.gov
|
|
* https://weather-gov.github.io/api/general-faqs
|
|
*
|
|
* Original by Vince Peri
|
|
* MIT Licensed.
|
|
*
|
|
* 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: function (config) {
|
|
this.config = config;
|
|
this.config.apiBase = "https://api.weather.gov";
|
|
this.fetchWxGovURLs(this.config);
|
|
},
|
|
|
|
// Called when the weather provider is about to start.
|
|
start: function () {
|
|
Log.info(`Weather provider: ${this.providerName} started.`);
|
|
},
|
|
|
|
// This returns the name of the fetched location or an empty string.
|
|
fetchedLocation: function () {
|
|
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}/points/${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;
|
|
// 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) {
|
|
const precipitationProbabilityRegEx = "Chance of precipitation is ([0-9]+?)%";
|
|
|
|
// 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 = [];
|
|
const precipitation = new RegExp(precipitationProbabilityRegEx, "g").exec(forecast.detailedForecast);
|
|
if (precipitation) weather.precipitationProbability = precipitation[1];
|
|
|
|
// 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;
|
|
}
|
|
});
|