first commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# Weather Module Weather Provider Development Documentation
|
||||
|
||||
For how to develop your own weather provider, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/development/weather-provider.html).
|
||||
@@ -0,0 +1,572 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Environment Canada (EC)
|
||||
*
|
||||
* This class is a provider for Environment Canada MSC Datamart
|
||||
* Note that this is only for Canadian locations and does not require an API key (access is anonymous)
|
||||
*
|
||||
* EC Documentation at following links:
|
||||
* https://dd.weather.gc.ca/citypage_weather/schema/
|
||||
* https://eccc-msc.github.io/open-data/msc-datamart/readme_en/
|
||||
*
|
||||
* This module supports Canadian locations only and requires 2 additional config parameters:
|
||||
*
|
||||
* siteCode - the city/town unique identifier for which weather is to be displayed. Format is 's0000000'.
|
||||
*
|
||||
* provCode - the 2-character province code for the selected city/town.
|
||||
*
|
||||
* Example: for Toronto, Ontario, the following parameters would be used
|
||||
*
|
||||
* siteCode: 's0000458',
|
||||
* provCode: 'ON'
|
||||
*
|
||||
* To determine the siteCode and provCode values for a Canadian city/town, look at the Environment Canada document
|
||||
* at https://dd.weather.gc.ca/citypage_weather/docs/site_list_en.csv (or site_list_fr.csv). There you will find a table
|
||||
* with locations you can search under column B (English Names), with the corresponding siteCode under
|
||||
* column A (Codes) and provCode under column C (Province).
|
||||
*
|
||||
* Original by Kevin Godin
|
||||
*
|
||||
* License to use Environment Canada (EC) data is detailed here:
|
||||
* https://eccc-msc.github.io/open-data/licence/readme_en/
|
||||
*
|
||||
*/
|
||||
|
||||
WeatherProvider.register("envcanada", {
|
||||
// Set the name of the provider for debugging and alerting purposes (eg. provide eye-catcher)
|
||||
providerName: "Environment Canada",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
siteCode: "s1234567",
|
||||
provCode: "ON"
|
||||
},
|
||||
|
||||
//
|
||||
// Set config values (equates to weather module config values). Also set values pertaining to caching of
|
||||
// Today's temperature forecast (for use in the Forecast functions below)
|
||||
//
|
||||
setConfig: function (config) {
|
||||
this.config = config;
|
||||
|
||||
this.todayTempCacheMin = 0;
|
||||
this.todayTempCacheMax = 0;
|
||||
this.todayCached = false;
|
||||
this.cacheCurrentTemp = 999;
|
||||
},
|
||||
|
||||
//
|
||||
// Called when the weather provider is started
|
||||
//
|
||||
start: function () {
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
this.setFetchedLocation(this.config.location);
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchCurrentWeather method to query EC and construct a Current weather object
|
||||
//
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada site data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
|
||||
|
||||
this.setWeatherForecast(forecastWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada forecast data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//
|
||||
// Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
|
||||
//
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl(), "xml")
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
return;
|
||||
}
|
||||
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
|
||||
|
||||
this.setWeatherHourly(hourlyWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load EnvCanada hourly data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Environment Canada methods - not part of the standard Provider methods
|
||||
//
|
||||
//////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
//
|
||||
// Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
|
||||
// URL defaults to the English version simply because there is no language dependency in the data
|
||||
// being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
|
||||
//
|
||||
getUrl() {
|
||||
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate a WeatherObject based on current EC weather conditions
|
||||
//
|
||||
|
||||
generateWeatherObjectFromCurrentWeather(ECdoc) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// There are instances where EC will update weather data and current temperature will not be
|
||||
// provided. While this is a defect in the EC systems, we need to accommodate to avoid a current temp
|
||||
// of NaN being displayed. Therefore... whenever we get a valid current temp from EC, we will cache
|
||||
// the value. Whenever EC data is missing current temp, we will provide the cached value
|
||||
// instead. This is reasonable since the cached value will typically be accurate within the previous
|
||||
// hour. The only time this does not work as expected is when MM is restarted and the first query to
|
||||
// EC finds no current temp. In this scenario, MM will end up displaying a current temp of null;
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions temperature").textContent) {
|
||||
currentWeather.temperature = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
this.cacheCurrentTemp = currentWeather.temperature;
|
||||
} else {
|
||||
currentWeather.temperature = this.cacheCurrentTemp;
|
||||
}
|
||||
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
|
||||
|
||||
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
|
||||
|
||||
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
|
||||
|
||||
// Ensure showPrecipitationAmount is forced to false. EC does not really provide POP for current day
|
||||
// and this feature for the weather module (current only) is sort of broken in that it wants
|
||||
// to say POP but will display precip as an accumulated amount vs. a percentage.
|
||||
|
||||
this.config.showPrecipitationAmount = false;
|
||||
|
||||
//
|
||||
// If the module config wants to showFeelsLike... default to the current temperature.
|
||||
// Check for EC wind chill and humidex values and overwrite the feelsLikeTemp value.
|
||||
// This assumes that the EC current conditions will never contain both a wind chill
|
||||
// and humidex temperature.
|
||||
//
|
||||
|
||||
if (this.config.showFeelsLike) {
|
||||
currentWeather.feelsLikeTemp = currentWeather.temperature;
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions windChill")) {
|
||||
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions windChill").textContent;
|
||||
}
|
||||
|
||||
if (ECdoc.querySelector("siteData currentConditions humidex")) {
|
||||
currentWeather.feelsLikeTemp = ECdoc.querySelector("siteData currentConditions humidex").textContent;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values
|
||||
//
|
||||
|
||||
currentWeather.weatherType = this.convertWeatherType(ECdoc.querySelector("siteData currentConditions iconCode").textContent);
|
||||
|
||||
//
|
||||
// Capture the sunrise and sunset values from EC data
|
||||
//
|
||||
|
||||
const sunList = ECdoc.querySelectorAll("siteData riseSet dateTime");
|
||||
|
||||
currentWeather.sunrise = moment(sunList[1].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
currentWeather.sunset = moment(sunList[3].querySelector("timeStamp").textContent, "YYYYMMDDhhmmss");
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromForecast(ECdoc) {
|
||||
// Declare an array to hold each day's forecast object
|
||||
|
||||
const days = [];
|
||||
|
||||
const weather = new WeatherObject();
|
||||
|
||||
const foreBaseDates = ECdoc.querySelectorAll("siteData forecastGroup dateTime");
|
||||
const baseDate = foreBaseDates[1].querySelector("timeStamp").textContent;
|
||||
|
||||
weather.date = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
const foreGroup = ECdoc.querySelectorAll("siteData forecastGroup forecast");
|
||||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
//
|
||||
// The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
|
||||
// 2 elements. the first element for a day details the Today (daytime) forecast while the second
|
||||
// element details the Tonight (nightime) forecast. Element 0 is always for the current day.
|
||||
//
|
||||
// However... the forecast is somewhat 'rolling'.
|
||||
//
|
||||
// If the EC forecast is queried in the morning, then Element 0 will contain Current
|
||||
// Today and Element 1 will contain Current Tonight. From there, the next 5 days of forecast will be
|
||||
// contained in Elements 2/3, 4/5, 6/7, 8/9, and 10/11. This module will create a 6-day forecast using
|
||||
// all of these Elements.
|
||||
//
|
||||
// But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
|
||||
// off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
|
||||
// Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
|
||||
// but only for the Today portion (not Tonight). This module will create a 6-day forecast using
|
||||
// Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
|
||||
//
|
||||
// We need to determine if Element 0 is showing the forecast for Current Today or Current Tonight.
|
||||
// This is required to understand how Min and Max temperature will be determined, and to understand
|
||||
// where the next day's (aka Tomorrow's) forecast is located in the forecast array.
|
||||
//
|
||||
|
||||
let nextDay = 0;
|
||||
let lastDay = 0;
|
||||
const currentTemp = ECdoc.querySelector("siteData currentConditions temperature").textContent;
|
||||
|
||||
//
|
||||
// If the first Element is Current Today, look at Current Today and Current Tonight for the current day.
|
||||
//
|
||||
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Today']")) {
|
||||
this.todaytempCacheMin = 0;
|
||||
this.todaytempCacheMax = 0;
|
||||
this.todayCached = true;
|
||||
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, true, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (Today and Tonight are present
|
||||
// in elements 0 and 11, we know that we will have 6 full days of forecasts and we will use
|
||||
// them. We will set lastDay such that we iterate through all 12 elements of the forecast.
|
||||
//
|
||||
|
||||
nextDay = 2;
|
||||
lastDay = 12;
|
||||
}
|
||||
|
||||
//
|
||||
// If the first Element is Current Tonight, look at Tonight only for the current day.
|
||||
//
|
||||
if (foreGroup[0].querySelector("period[textForecastName='Tonight']")) {
|
||||
this.setMinMaxTemps(weather, foreGroup, 0, false, currentTemp);
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, 0);
|
||||
|
||||
//
|
||||
// Set the Element number that will reflect where the next day's forecast is located. Also set
|
||||
// the Element number where the end of the forecast will be. This is important because of the
|
||||
// rolling nature of the EC forecast. In the current scenario (only Current Tonight is present
|
||||
// in Element 0, we know that we will have 6 full days of forecasts PLUS a half-day and
|
||||
// forecast in the final element. Because we will only use full day forecasts, we set the
|
||||
// lastDay number to ensure we ignore that final half-day (in forecast Element 11).
|
||||
//
|
||||
|
||||
nextDay = 1;
|
||||
lastDay = 11;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element's icon to
|
||||
// reflect either Today or Tonight depending on what the forecast is showing in Element 0.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[0].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
|
||||
//
|
||||
// Now do the rest of the forecast starting at nextDay. We will process each day using 2 EC
|
||||
// forecast Elements. This will address the fact that the EC forecast always includes Today and
|
||||
// Tonight for each day. This is why we iterate through the forecast by a a count of 2, with each
|
||||
// iteration looking at the current Element and the next Element.
|
||||
//
|
||||
|
||||
let lastDate = moment(baseDate, "YYYYMMDDhhmmss");
|
||||
|
||||
for (let stepDay = nextDay; stepDay < lastDay; stepDay += 2) {
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// Add 1 to the date to reflect the current forecast day we are building
|
||||
|
||||
lastDate = lastDate.add(1, "day");
|
||||
weather.date = moment(lastDate);
|
||||
|
||||
// Capture the temperatures for the current Element and the next Element in order to set
|
||||
// the Min and Max temperatures for the forecast
|
||||
|
||||
this.setMinMaxTemps(weather, foreGroup, stepDay, true, currentTemp);
|
||||
|
||||
weather.precipitationAmount = null;
|
||||
|
||||
this.setPrecipitation(weather, foreGroup, stepDay);
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(foreGroup[stepDay].querySelector("abbreviatedForecast iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
//
|
||||
// Generate an array of WeatherObjects based on EC hourly weather forecast
|
||||
//
|
||||
|
||||
generateWeatherObjectsFromHourly(ECdoc) {
|
||||
// Declare an array to hold each hour's forecast object
|
||||
|
||||
const hours = [];
|
||||
|
||||
// Get local timezone UTC offset so that each hourly time can be calculated properly
|
||||
|
||||
const baseHours = ECdoc.querySelectorAll("siteData hourlyForecastGroup dateTime");
|
||||
const hourOffset = baseHours[1].getAttribute("UTCOffset");
|
||||
|
||||
//
|
||||
// The EC hourly forecast is held in a 24-element array - Elements 0 to 23 - with Element 0 holding
|
||||
// the forecast for the next 'on the hour' timeslot. This means the array is a rolling 24 hours.
|
||||
//
|
||||
|
||||
const hourGroup = ECdoc.querySelectorAll("siteData hourlyForecastGroup hourlyForecast");
|
||||
|
||||
for (let stepHour = 0; stepHour < 24; stepHour += 1) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// Determine local time by applying UTC offset to the forecast timestamp
|
||||
|
||||
const foreTime = moment(hourGroup[stepHour].getAttribute("dateTimeUTC"), "YYYYMMDDhhmmss");
|
||||
const currTime = foreTime.add(hourOffset, "hours");
|
||||
weather.date = moment(currTime);
|
||||
|
||||
// Capture the temperature
|
||||
|
||||
weather.temperature = hourGroup[stepHour].querySelector("temperature").textContent;
|
||||
|
||||
// Capture Likelihood of Precipitation (LOP) and unit-of-measure values
|
||||
|
||||
const precipLOP = hourGroup[stepHour].querySelector("lop").textContent * 1.0;
|
||||
|
||||
if (precipLOP > 0) {
|
||||
weather.precipitationProbability = precipLOP;
|
||||
}
|
||||
|
||||
//
|
||||
// Need to map EC weather icon to MM weatherType values. Always pick the first Element icon.
|
||||
//
|
||||
|
||||
weather.weatherType = this.convertWeatherType(hourGroup[stepHour].querySelector("iconCode").textContent);
|
||||
|
||||
// Push the weather object into the forecast array.
|
||||
|
||||
hours.push(weather);
|
||||
}
|
||||
|
||||
return hours;
|
||||
},
|
||||
//
|
||||
// Determine Min and Max temp based on a supplied Forecast Element index and a boolen that denotes if
|
||||
// the next Forecast element should be considered - i.e. look at Today *and* Tonight vs.Tonight-only
|
||||
//
|
||||
|
||||
setMinMaxTemps(weather, foreGroup, today, fullDay, currentTemp) {
|
||||
const todayTemp = foreGroup[today].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const todayClass = foreGroup[today].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
//
|
||||
// The following logic is largely aimed at accommodating the Current day's forecast whereby we
|
||||
// can have either Current Today+Current Tonight or only Current Tonight.
|
||||
//
|
||||
// If fullDay is false, then we only have Tonight for the current day's forecast - meaning we have
|
||||
// lost a min or max temp value for the day. Therefore, we will see if we were able to cache the the
|
||||
// Today forecast for the current day. If we have, we will use them. If we do not have the cached values,
|
||||
// it means that MM or the Computer has been restarted since the time EC rolled off Today from the
|
||||
// forecast. In this scenario, we will simply default to the Current Conditions temperature and then
|
||||
// check the Tonight temperature.
|
||||
//
|
||||
|
||||
if (fullDay === false) {
|
||||
if (this.todayCached === true) {
|
||||
weather.minTemperature = this.todayTempCacheMin;
|
||||
weather.maxTemperature = this.todayTempCacheMax;
|
||||
} else {
|
||||
weather.minTemperature = currentTemp;
|
||||
weather.maxTemperature = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// We will check to see if the current Element's temperature is Low or High and set weather values
|
||||
// accordingly. We will also check the condition where fullDay is true *and* we are looking at forecast
|
||||
// element 0. This is a special case where we will cache temperature values so that we have them later
|
||||
// in the current day when the Current Today element rolls off and we have Current Tonight only.
|
||||
//
|
||||
|
||||
if (todayClass === "low") {
|
||||
weather.minTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMin = weather.minTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
if (todayClass === "high") {
|
||||
weather.maxTemperature = todayTemp;
|
||||
if (today === 0 && fullDay === true) {
|
||||
this.todayTempCacheMax = weather.maxTemperature;
|
||||
}
|
||||
}
|
||||
|
||||
const nextTemp = foreGroup[today + 1].querySelector("temperatures temperature").textContent;
|
||||
|
||||
const nextClass = foreGroup[today + 1].querySelector("temperatures temperature").getAttribute("class");
|
||||
|
||||
if (fullDay === true) {
|
||||
if (nextClass === "low") {
|
||||
weather.minTemperature = nextTemp;
|
||||
}
|
||||
|
||||
if (nextClass === "high") {
|
||||
weather.maxTemperature = nextTemp;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Check for a Precipitation forecast. EC can provide a forecast in 2 ways: either an accumulation figure
|
||||
// or a POP percentage. If there is a POP, then that is what the module will show. If there is an accumulation,
|
||||
// then it will be displayed ONLY if no POP is present.
|
||||
//
|
||||
// POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
|
||||
// people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
|
||||
// (if one exists) in that specific scenario.
|
||||
//
|
||||
// Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
|
||||
// people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
|
||||
// of each day, the weather module does not really allow for that view of a daily forecast. There we will
|
||||
// ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
|
||||
// the nightime forecast after a certain point in that specific scenario.
|
||||
//
|
||||
|
||||
setPrecipitation(weather, foreGroup, today) {
|
||||
if (foreGroup[today].querySelector("precipitation accumulation")) {
|
||||
weather.precipitationAmount = foreGroup[today].querySelector("precipitation accumulation amount").textContent * 1.0;
|
||||
weather.precipitationUnits = foreGroup[today].querySelector("precipitation accumulation amount").getAttribute("units");
|
||||
}
|
||||
|
||||
// Check Today element for POP
|
||||
const precipPOP = foreGroup[today].querySelector("abbreviatedForecast pop").textContent * 1.0;
|
||||
if (precipPOP > 0) {
|
||||
weather.precipitationProbability = precipPOP;
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
// Convert the icons to a more usable name.
|
||||
//
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"00": "day-sunny",
|
||||
"01": "day-sunny",
|
||||
"02": "day-sunny-overcast",
|
||||
"03": "day-cloudy",
|
||||
"04": "day-cloudy",
|
||||
"05": "day-cloudy",
|
||||
"06": "day-sprinkle",
|
||||
"07": "day-showers",
|
||||
"08": "day-snow",
|
||||
"09": "day-thunderstorm",
|
||||
10: "cloud",
|
||||
11: "showers",
|
||||
12: "rain",
|
||||
13: "rain",
|
||||
14: "sleet",
|
||||
15: "sleet",
|
||||
16: "snow",
|
||||
17: "snow",
|
||||
18: "snow",
|
||||
19: "thunderstorm",
|
||||
20: "cloudy",
|
||||
21: "cloudy",
|
||||
22: "day-cloudy",
|
||||
23: "day-haze",
|
||||
24: "fog",
|
||||
25: "snow-wind",
|
||||
26: "sleet",
|
||||
27: "sleet",
|
||||
28: "rain",
|
||||
29: "na",
|
||||
30: "night-clear",
|
||||
31: "night-clear",
|
||||
32: "night-partly-cloudy",
|
||||
33: "night-alt-cloudy",
|
||||
34: "night-alt-cloudy",
|
||||
35: "night-partly-cloudy",
|
||||
36: "night-alt-showers",
|
||||
37: "night-rain-mix",
|
||||
38: "night-alt-snow",
|
||||
39: "night-thunderstorm",
|
||||
40: "snow-wind",
|
||||
41: "tornado",
|
||||
42: "tornado",
|
||||
43: "windy",
|
||||
44: "smoke",
|
||||
45: "sandstorm",
|
||||
46: "thunderstorm",
|
||||
47: "thunderstorm",
|
||||
48: "tornado"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,548 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Open-Meteo
|
||||
*
|
||||
* By Andrés Vanegas
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Open-Meteo, based on Andrew Pometti's class
|
||||
* for Weatherbit.
|
||||
*/
|
||||
// https://www.bigdatacloud.com/docs/api/free-reverse-geocode-to-city-api
|
||||
const GEOCODE_BASE = "https://api.bigdatacloud.net/data/reverse-geocode-client";
|
||||
const OPEN_METEO_BASE = "https://api.open-meteo.com/v1";
|
||||
|
||||
WeatherProvider.register("openmeteo", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Open-Meteo",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: OPEN_METEO_BASE,
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
pastDays: 0,
|
||||
type: "current"
|
||||
},
|
||||
|
||||
// https://open-meteo.com/en/docs
|
||||
hourlyParams: [
|
||||
// Air temperature at 2 meters above ground
|
||||
"temperature_2m",
|
||||
// Relative humidity at 2 meters above ground
|
||||
"relativehumidity_2m",
|
||||
// Dew point temperature at 2 meters above ground
|
||||
"dewpoint_2m",
|
||||
// Apparent temperature is the perceived feels-like temperature combining wind chill factor, relative humidity and solar radiation
|
||||
"apparent_temperature",
|
||||
// Atmospheric air pressure reduced to mean sea level (msl) or pressure at surface. Typically pressure on mean sea level is used in meteorology. Surface pressure gets lower with increasing elevation.
|
||||
"pressure_msl",
|
||||
"surface_pressure",
|
||||
// Total cloud cover as an area fraction
|
||||
"cloudcover",
|
||||
// Low level clouds and fog up to 3 km altitude
|
||||
"cloudcover_low",
|
||||
// Mid level clouds from 3 to 8 km altitude
|
||||
"cloudcover_mid",
|
||||
// High level clouds from 8 km altitude
|
||||
"cloudcover_high",
|
||||
// Wind speed at 10, 80, 120 or 180 meters above ground. Wind speed on 10 meters is the standard level.
|
||||
"windspeed_10m",
|
||||
"windspeed_80m",
|
||||
"windspeed_120m",
|
||||
"windspeed_180m",
|
||||
// Wind direction at 10, 80, 120 or 180 meters above ground
|
||||
"winddirection_10m",
|
||||
"winddirection_80m",
|
||||
"winddirection_120m",
|
||||
"winddirection_180m",
|
||||
// Gusts at 10 meters above ground as a maximum of the preceding hour
|
||||
"windgusts_10m",
|
||||
// Shortwave solar radiation as average of the preceding hour. This is equal to the total global horizontal irradiation
|
||||
"shortwave_radiation",
|
||||
// Direct solar radiation as average of the preceding hour on the horizontal plane and the normal plane (perpendicular to the sun)
|
||||
"direct_radiation",
|
||||
"direct_normal_irradiance",
|
||||
// Diffuse solar radiation as average of the preceding hour
|
||||
"diffuse_radiation",
|
||||
// Vapor Pressure Deificit (VPD) in kilopascal (kPa). For high VPD (>1.6), water transpiration of plants increases. For low VPD (<0.4), transpiration decreases
|
||||
"vapor_pressure_deficit",
|
||||
// Evapotranspration from land surface and plants that weather models assumes for this location. Available soil water is considered. 1 mm evapotranspiration per hour equals 1 liter of water per spare meter.
|
||||
"evapotranspiration",
|
||||
// ET₀ Reference Evapotranspiration of a well watered grass field. Based on FAO-56 Penman-Monteith equations ET₀ is calculated from temperature, wind speed, humidity and solar radiation. Unlimited soil water is assumed. ET₀ is commonly used to estimate the required irrigation for plants.
|
||||
"et0_fao_evapotranspiration",
|
||||
// Total precipitation (rain, showers, snow) sum of the preceding hour
|
||||
"precipitation",
|
||||
// Precipitation Probability
|
||||
"precipitation_probability",
|
||||
// UV index
|
||||
"uv_index",
|
||||
// Snowfall amount of the preceding hour in centimeters. For the water equivalent in millimeter, divide by 7. E.g. 7 cm snow = 10 mm precipitation water equivalent
|
||||
"snowfall",
|
||||
// Rain from large scale weather systems of the preceding hour in millimeter
|
||||
"rain",
|
||||
// Showers from convective precipitation in millimeters from the preceding hour
|
||||
"showers",
|
||||
// Weather condition as a numeric code. Follow WMO weather interpretation codes.
|
||||
"weathercode",
|
||||
// Snow depth on the ground
|
||||
"snow_depth",
|
||||
// Altitude above sea level of the 0°C level
|
||||
"freezinglevel_height",
|
||||
// Temperature in the soil at 0, 6, 18 and 54 cm depths. 0 cm is the surface temperature on land or water surface temperature on water.
|
||||
"soil_temperature_0cm",
|
||||
"soil_temperature_6cm",
|
||||
"soil_temperature_18cm",
|
||||
"soil_temperature_54cm",
|
||||
// Average soil water content as volumetric mixing ratio at 0-1, 1-3, 3-9, 9-27 and 27-81 cm depths.
|
||||
"soil_moisture_0_1cm",
|
||||
"soil_moisture_1_3cm",
|
||||
"soil_moisture_3_9cm",
|
||||
"soil_moisture_9_27cm",
|
||||
"soil_moisture_27_81cm"
|
||||
],
|
||||
|
||||
dailyParams: [
|
||||
// Maximum and minimum daily air temperature at 2 meters above ground
|
||||
"temperature_2m_max",
|
||||
"temperature_2m_min",
|
||||
// Maximum and minimum daily apparent temperature
|
||||
"apparent_temperature_min",
|
||||
"apparent_temperature_max",
|
||||
// Sum of daily precipitation (including rain, showers and snowfall)
|
||||
"precipitation_sum",
|
||||
// Sum of daily rain
|
||||
"rain_sum",
|
||||
// Sum of daily showers
|
||||
"showers_sum",
|
||||
// Sum of daily snowfall
|
||||
"snowfall_sum",
|
||||
// The number of hours with rain
|
||||
"precipitation_hours",
|
||||
// The most severe weather condition on a given day
|
||||
"weathercode",
|
||||
// Sun rise and set times
|
||||
"sunrise",
|
||||
"sunset",
|
||||
// Maximum wind speed and gusts on a day
|
||||
"windspeed_10m_max",
|
||||
"windgusts_10m_max",
|
||||
// Dominant wind direction
|
||||
"winddirection_10m_dominant",
|
||||
// The sum of solar radiation on a given day in Megajoules
|
||||
"shortwave_radiation_sum",
|
||||
//UV Index
|
||||
"uv_index_max",
|
||||
// Daily sum of ET₀ Reference Evapotranspiration of a well watered grass field
|
||||
"et0_fao_evapotranspiration"
|
||||
],
|
||||
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(parsedData);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const dailyForecast = this.generateWeatherObjectsFromForecast(parsedData);
|
||||
this.setWeatherForecast(dailyForecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => this.parseWeatherApiResponse(data))
|
||||
.then((parsedData) => {
|
||||
if (!parsedData) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const hourlyForecast = this.generateWeatherObjectsFromHourly(parsedData);
|
||||
this.setWeatherHourly(hourlyForecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = {
|
||||
lang: config.lang ?? "en",
|
||||
...this.defaults,
|
||||
...config
|
||||
};
|
||||
|
||||
// Set properly maxNumberOfDays and max Entries properties according to config and value ranges allowed in the documentation
|
||||
const maxEntriesLimit = ["daily", "forecast"].includes(this.config.type) ? 7 : this.config.type === "hourly" ? 48 : 0;
|
||||
if (this.config.hasOwnProperty("maxNumberOfDays") && !isNaN(parseFloat(this.config.maxNumberOfDays))) {
|
||||
const daysFactor = ["daily", "forecast"].includes(this.config.type) ? 1 : this.config.type === "hourly" ? 24 : 0;
|
||||
this.config.maxEntries = Math.max(1, Math.min(Math.round(parseFloat(this.config.maxNumberOfDays)) * daysFactor, maxEntriesLimit));
|
||||
this.config.maxNumberOfDays = Math.ceil(this.config.maxEntries / Math.max(1, daysFactor));
|
||||
}
|
||||
this.config.maxEntries = Math.max(1, Math.min(this.config.maxEntries, maxEntriesLimit));
|
||||
|
||||
if (!this.config.type) {
|
||||
Log.error("type not configured and could not resolve it");
|
||||
}
|
||||
|
||||
this.fetchLocation();
|
||||
},
|
||||
|
||||
// Generate valid query params to perform the request
|
||||
getQueryParameters() {
|
||||
let params = {
|
||||
latitude: this.config.lat,
|
||||
longitude: this.config.lon,
|
||||
timeformat: "unixtime",
|
||||
timezone: "auto",
|
||||
past_days: this.config.pastDays ?? 0,
|
||||
daily: this.dailyParams,
|
||||
hourly: this.hourlyParams,
|
||||
// Fixed units as metric
|
||||
temperature_unit: "celsius",
|
||||
windspeed_unit: "ms",
|
||||
precipitation_unit: "mm"
|
||||
};
|
||||
|
||||
const startDate = moment().startOf("day");
|
||||
const endDate = moment(startDate)
|
||||
.add(Math.max(0, Math.min(7, this.config.maxNumberOfDays)), "days")
|
||||
.endOf("day");
|
||||
|
||||
params["start_date"] = startDate.format("YYYY-MM-DD");
|
||||
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
case "forecast":
|
||||
params["end_date"] = endDate.format("YYYY-MM-DD");
|
||||
break;
|
||||
case "current":
|
||||
params["current_weather"] = true;
|
||||
params["end_date"] = params["start_date"];
|
||||
break;
|
||||
default:
|
||||
// Failsafe
|
||||
return "";
|
||||
}
|
||||
|
||||
return Object.keys(params)
|
||||
.filter((key) => (params[key] ? true : false))
|
||||
.map((key) => {
|
||||
switch (key) {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
return `${encodeURIComponent(key)}=${params[key].join(",")}`;
|
||||
default:
|
||||
return `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`;
|
||||
}
|
||||
})
|
||||
.join("&");
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
return `${this.config.apiBase}/forecast?${this.getQueryParameters()}`;
|
||||
},
|
||||
|
||||
// Transpose hourly and daily data matrices
|
||||
transposeDataMatrix(data) {
|
||||
return data.time.map((_, index) =>
|
||||
Object.keys(data).reduce((row, key) => {
|
||||
return {
|
||||
...row,
|
||||
// Parse time values as momentjs instances
|
||||
[key]: ["time", "sunrise", "sunset"].includes(key) ? moment.unix(data[key][index]) : data[key][index]
|
||||
};
|
||||
}, {})
|
||||
);
|
||||
},
|
||||
|
||||
// Sanitize and validate API response
|
||||
parseWeatherApiResponse(data) {
|
||||
const validByType = {
|
||||
current: data.current_weather && data.current_weather.time,
|
||||
hourly: data.hourly && data.hourly.time && Array.isArray(data.hourly.time) && data.hourly.time.length > 0,
|
||||
daily: data.daily && data.daily.time && Array.isArray(data.daily.time) && data.daily.time.length > 0
|
||||
};
|
||||
// backwards compatibility
|
||||
const type = ["daily", "forecast"].includes(this.config.type) ? "daily" : this.config.type;
|
||||
|
||||
if (!validByType[type]) return;
|
||||
|
||||
switch (type) {
|
||||
case "current":
|
||||
if (!validByType.daily && !validByType.hourly) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "hourly":
|
||||
case "daily":
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of ["hourly", "daily"]) {
|
||||
if (typeof data[key] === "object") {
|
||||
data[key] = this.transposeDataMatrix(data[key]);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.current_weather) {
|
||||
data.current_weather.time = moment.unix(data.current_weather.time);
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
// Reverse geocoding from latitude and longitude provided
|
||||
fetchLocation() {
|
||||
this.fetchData(`${GEOCODE_BASE}?latitude=${this.config.lat}&longitude=${this.config.lon}&localityLanguage=${this.config.lang}`)
|
||||
.then((data) => {
|
||||
if (!data || !data.city) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
this.fetchedLocationName = `${data.city}, ${data.principalSubdivisionCode}`;
|
||||
})
|
||||
.catch((request) => {
|
||||
Log.error("Could not load data ... ", request);
|
||||
});
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(weather) {
|
||||
/**
|
||||
* Since some units comes from API response "splitted" into daily, hourly and current_weather
|
||||
* every time you request it, you have to ensure to get the data from the right place every time.
|
||||
* For the current weather case, the response have the following structure (after transposing):
|
||||
* ```
|
||||
* {
|
||||
* current_weather: { ...<some current weather here> },
|
||||
* hourly: [
|
||||
* 0: {...<data for hour zero here> },
|
||||
* 1: {...<data for hour one here> },
|
||||
* ...
|
||||
* ],
|
||||
* daily: [
|
||||
* {...<summary data for current day here> },
|
||||
* ]
|
||||
* }
|
||||
* ```
|
||||
* Some data should be returned from `hourly` array data when the index matches the current hour,
|
||||
* some data from the first and only one object received in `daily` array and some from the
|
||||
* `current_weather` object.
|
||||
*/
|
||||
const h = moment().hour();
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = weather.current_weather.time;
|
||||
currentWeather.windSpeed = weather.current_weather.windspeed;
|
||||
currentWeather.windFromDirection = weather.current_weather.winddirection;
|
||||
currentWeather.sunrise = weather.daily[0].sunrise;
|
||||
currentWeather.sunset = weather.daily[0].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.current_weather.temperature);
|
||||
currentWeather.minTemperature = parseFloat(weather.daily[0].temperature_2m_min);
|
||||
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.hourly[h].rain);
|
||||
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.hourly[h].precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.hourly[h].uv_index);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
// Implement WeatherForecast generator.
|
||||
generateWeatherObjectsFromForecast(weathers) {
|
||||
const days = [];
|
||||
|
||||
weathers.daily.forEach((weather, i) => {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m_max;
|
||||
currentWeather.windFromDirection = weather.winddirection_10m_dominant;
|
||||
currentWeather.sunrise = weather.sunrise;
|
||||
currentWeather.sunset = weather.sunset;
|
||||
currentWeather.temperature = parseFloat((weather.apparent_temperature_max + weather.apparent_temperature_min) / 2);
|
||||
currentWeather.minTemperature = parseFloat(weather.apparent_temperature_min);
|
||||
currentWeather.maxTemperature = parseFloat(weather.apparent_temperature_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.rain = parseFloat(weather.rain_sum);
|
||||
currentWeather.snow = parseFloat(weather.snowfall_sum * 10);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.precipitation_sum);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.uv_index_max);
|
||||
|
||||
days.push(currentWeather);
|
||||
});
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Implement WeatherHourly generator.
|
||||
generateWeatherObjectsFromHourly(weathers) {
|
||||
const hours = [];
|
||||
const now = moment();
|
||||
|
||||
weathers.hourly.forEach((weather, i) => {
|
||||
if ((hours.length === 0 && weather.time.hour() <= now.hour()) || hours.length >= this.config.maxEntries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = new WeatherObject();
|
||||
const h = Math.ceil((i + 1) / 24) - 1;
|
||||
|
||||
currentWeather.date = weather.time;
|
||||
currentWeather.windSpeed = weather.windspeed_10m;
|
||||
currentWeather.windFromDirection = weather.winddirection_10m;
|
||||
currentWeather.sunrise = weathers.daily[h].sunrise;
|
||||
currentWeather.sunset = weathers.daily[h].sunset;
|
||||
currentWeather.temperature = parseFloat(weather.apparent_temperature);
|
||||
currentWeather.minTemperature = parseFloat(weathers.daily[h].apparent_temperature_min);
|
||||
currentWeather.maxTemperature = parseFloat(weathers.daily[h].apparent_temperature_max);
|
||||
currentWeather.weatherType = this.convertWeatherType(weather.weathercode, currentWeather.isDayTime());
|
||||
currentWeather.humidity = parseFloat(weather.relativehumidity_2m);
|
||||
currentWeather.rain = parseFloat(weather.rain);
|
||||
currentWeather.snow = parseFloat(weather.snowfall * 10);
|
||||
currentWeather.precipitationAmount = parseFloat(weather.precipitation);
|
||||
currentWeather.precipitationProbability = parseFloat(weather.precipitation_probability);
|
||||
currentWeather.uv_index = parseFloat(weather.uv_index);
|
||||
|
||||
hours.push(currentWeather);
|
||||
});
|
||||
|
||||
return hours;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weathercode, isDayTime) {
|
||||
const weatherConditions = {
|
||||
0: "clear",
|
||||
1: "mainly-clear",
|
||||
2: "partly-cloudy",
|
||||
3: "overcast",
|
||||
45: "fog",
|
||||
48: "depositing-rime-fog",
|
||||
51: "drizzle-light-intensity",
|
||||
53: "drizzle-moderate-intensity",
|
||||
55: "drizzle-dense-intensity",
|
||||
56: "freezing-drizzle-light-intensity",
|
||||
57: "freezing-drizzle-dense-intensity",
|
||||
61: "rain-slight-intensity",
|
||||
63: "rain-moderate-intensity",
|
||||
65: "rain-heavy-intensity",
|
||||
66: "freezing-rain-light-heavy-intensity",
|
||||
67: "freezing-rain-heavy-intensity",
|
||||
71: "snow-fall-slight-intensity",
|
||||
73: "snow-fall-moderate-intensity",
|
||||
75: "snow-fall-heavy-intensity",
|
||||
77: "snow-grains",
|
||||
80: "rain-showers-slight",
|
||||
81: "rain-showers-moderate",
|
||||
82: "rain-showers-violent",
|
||||
85: "snow-showers-slight",
|
||||
86: "snow-showers-heavy",
|
||||
95: "thunderstorm",
|
||||
96: "thunderstorm-slight-hail",
|
||||
99: "thunderstorm-heavy-hail"
|
||||
};
|
||||
|
||||
if (!Object.keys(weatherConditions).includes(`${weathercode}`)) return null;
|
||||
|
||||
switch (weatherConditions[`${weathercode}`]) {
|
||||
case "clear":
|
||||
return isDayTime ? "day-sunny" : "night-clear";
|
||||
case "mainly-clear":
|
||||
case "partly-cloudy":
|
||||
return isDayTime ? "day-cloudy" : "night-alt-cloudy";
|
||||
case "overcast":
|
||||
return isDayTime ? "day-sunny-overcast" : "night-alt-partly-cloudy";
|
||||
case "fog":
|
||||
case "depositing-rime-fog":
|
||||
return isDayTime ? "day-fog" : "night-fog";
|
||||
case "drizzle-light-intensity":
|
||||
case "rain-slight-intensity":
|
||||
case "rain-showers-slight":
|
||||
return isDayTime ? "day-sprinkle" : "night-sprinkle";
|
||||
case "drizzle-moderate-intensity":
|
||||
case "rain-moderate-intensity":
|
||||
case "rain-showers-moderate":
|
||||
return isDayTime ? "day-showers" : "night-showers";
|
||||
case "drizzle-dense-intensity":
|
||||
case "rain-heavy-intensity":
|
||||
case "rain-showers-violent":
|
||||
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||
case "freezing-rain-light-intensity":
|
||||
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||
case "freezing-drizzle-light-intensity":
|
||||
case "freezing-drizzle-dense-intensity":
|
||||
return "snowflake-cold";
|
||||
case "snow-grains":
|
||||
return isDayTime ? "day-sleet" : "night-sleet";
|
||||
case "snow-fall-slight-intensity":
|
||||
case "snow-fall-moderate-intensity":
|
||||
return isDayTime ? "day-snow-wind" : "night-snow-wind";
|
||||
case "snow-fall-heavy-intensity":
|
||||
case "freezing-rain-heavy-intensity":
|
||||
return isDayTime ? "day-snow-thunderstorm" : "night-snow-thunderstorm";
|
||||
case "snow-showers-slight":
|
||||
case "snow-showers-heavy":
|
||||
return isDayTime ? "day-rain-mix" : "night-rain-mix";
|
||||
case "thunderstorm":
|
||||
return isDayTime ? "day-thunderstorm" : "night-thunderstorm";
|
||||
case "thunderstorm-slight-hail":
|
||||
return isDayTime ? "day-sleet" : "night-sleet";
|
||||
case "thunderstorm-heavy-hail":
|
||||
return isDayTime ? "day-sleet-storm" : "night-sleet-storm";
|
||||
default:
|
||||
return "na";
|
||||
}
|
||||
},
|
||||
|
||||
// Define required scripts.
|
||||
getScripts: function () {
|
||||
return ["moment.js"];
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,448 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Michael Teeuw https://michaelteeuw.nl
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is the blueprint for a weather provider.
|
||||
*/
|
||||
WeatherProvider.register("openweathermap", {
|
||||
// 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: "OpenWeatherMap",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiVersion: "2.5",
|
||||
apiBase: "https://api.openweathermap.org/data/",
|
||||
weatherEndpoint: "", // can be "onecall", "forecast" or "weather" (for current)
|
||||
locationID: false,
|
||||
location: false,
|
||||
lat: 0, // the onecall endpoint needs lat / lon values, it doesn't support the locationId
|
||||
lon: 0,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let currentWeather;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
currentWeather = this.generateWeatherObjectsFromOnecall(data).current;
|
||||
this.setFetchedLocation(`${data.timezone}`);
|
||||
} else {
|
||||
currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
}
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherForecast method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
let forecast;
|
||||
let location;
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
forecast = this.generateWeatherObjectsFromOnecall(data).days;
|
||||
location = `${data.timezone}`;
|
||||
} else {
|
||||
forecast = this.generateWeatherObjectsFromForecast(data.list);
|
||||
location = `${data.city.name}, ${data.city.country}`;
|
||||
}
|
||||
this.setWeatherForecast(forecast);
|
||||
this.setFetchedLocation(location);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchWeatherHourly method.
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`(${data.lat},${data.lon})`);
|
||||
|
||||
const weatherData = this.generateWeatherObjectsFromOnecall(data);
|
||||
this.setWeatherHourly(weatherData.hours);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
this.config.weatherEndpoint = "/onecall";
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.config.weatherEndpoint = "/forecast";
|
||||
break;
|
||||
case "current":
|
||||
this.config.weatherEndpoint = "/weather";
|
||||
break;
|
||||
default:
|
||||
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl() {
|
||||
return this.config.apiBase + this.config.apiVersion + this.config.weatherEndpoint + this.getParams();
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment.unix(currentWeatherData.dt);
|
||||
currentWeather.humidity = currentWeatherData.main.humidity;
|
||||
currentWeather.temperature = currentWeatherData.main.temp;
|
||||
currentWeather.feelsLikeTemp = currentWeatherData.main.feels_like;
|
||||
currentWeather.windSpeed = currentWeatherData.wind.speed;
|
||||
currentWeather.windFromDirection = currentWeatherData.wind.deg;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.weather[0].icon);
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.sys.sunrise);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.sys.sunset);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
if (this.config.weatherEndpoint === "/forecast") {
|
||||
return this.generateForecastHourly(forecasts);
|
||||
} else if (this.config.weatherEndpoint === "/forecast/daily") {
|
||||
return this.generateForecastDaily(forecasts);
|
||||
}
|
||||
// if weatherEndpoint does not match forecast or forecast/daily, what should be returned?
|
||||
return [new WeatherObject()];
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on One Call forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromOnecall(data) {
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
return this.fetchOnecall(data);
|
||||
}
|
||||
// if weatherEndpoint does not match onecall, what should be returned?
|
||||
return { current: new WeatherObject(), hours: [], days: [] };
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate forecast information for 3-hourly forecast (available for free
|
||||
* subscription).
|
||||
*/
|
||||
generateForecastHourly(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
// variables for temperature range and rain
|
||||
let minTemp = [];
|
||||
let maxTemp = [];
|
||||
let rain = 0;
|
||||
let snow = 0;
|
||||
// variable for date
|
||||
let date = "";
|
||||
let weather = new WeatherObject();
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
if (date !== moment.unix(forecast.dt).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);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
// create new weather-object
|
||||
weather = new WeatherObject();
|
||||
|
||||
minTemp = [];
|
||||
maxTemp = [];
|
||||
rain = 0;
|
||||
snow = 0;
|
||||
|
||||
// set new date
|
||||
date = moment.unix(forecast.dt).format("YYYY-MM-DD");
|
||||
|
||||
// specify date
|
||||
weather.date = moment.unix(forecast.dt);
|
||||
|
||||
// If the first value of today is later than 17:00, we have an icon at least!
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
if (moment.unix(forecast.dt).format("H") >= 8 && moment.unix(forecast.dt).format("H") <= 17) {
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
}
|
||||
|
||||
// the same day as before
|
||||
// add values from forecast to corresponding variables
|
||||
minTemp.push(forecast.main.temp_min);
|
||||
maxTemp.push(forecast.main.temp_max);
|
||||
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain["3h"])) {
|
||||
rain += forecast.rain["3h"];
|
||||
}
|
||||
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow["3h"])) {
|
||||
snow += forecast.snow["3h"];
|
||||
}
|
||||
}
|
||||
|
||||
// last day
|
||||
// calculate minimum/maximum temperature, specify rain amount
|
||||
weather.minTemperature = Math.min.apply(null, minTemp);
|
||||
weather.maxTemperature = Math.max.apply(null, maxTemp);
|
||||
weather.rain = rain;
|
||||
weather.snow = snow;
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
// push weather information to days array
|
||||
days.push(weather);
|
||||
return days.slice(1);
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate forecast information for daily forecast (available for paid
|
||||
* subscription or old apiKey).
|
||||
*/
|
||||
generateForecastDaily(forecasts) {
|
||||
// initial variable declaration
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment.unix(forecast.dt);
|
||||
weather.minTemperature = forecast.temp.min;
|
||||
weather.maxTemperature = forecast.temp.max;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather[0].icon);
|
||||
weather.rain = 0;
|
||||
weather.snow = 0;
|
||||
|
||||
// forecast.rain not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("rain") && !isNaN(forecast.rain)) {
|
||||
weather.rain = forecast.rain;
|
||||
}
|
||||
|
||||
// forecast.snow not available if amount is zero
|
||||
// The API always returns in millimeters
|
||||
if (forecast.hasOwnProperty("snow") && !isNaN(forecast.snow)) {
|
||||
weather.snow = forecast.snow;
|
||||
}
|
||||
|
||||
weather.precipitationAmount = weather.rain + weather.snow;
|
||||
weather.precipitationProbability = forecast.pop ? forecast.pop * 100 : undefined;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Fetch One Call forecast information (available for free subscription).
|
||||
* Factors in timezone offsets.
|
||||
* Minutely forecasts are excluded for the moment, see getParams().
|
||||
*/
|
||||
fetchOnecall(data) {
|
||||
let precip = false;
|
||||
|
||||
// get current weather, if requested
|
||||
const current = new WeatherObject();
|
||||
if (data.hasOwnProperty("current")) {
|
||||
current.date = moment.unix(data.current.dt).utcOffset(data.timezone_offset / 60);
|
||||
current.windSpeed = data.current.wind_speed;
|
||||
current.windFromDirection = data.current.wind_deg;
|
||||
current.sunrise = moment.unix(data.current.sunrise).utcOffset(data.timezone_offset / 60);
|
||||
current.sunset = moment.unix(data.current.sunset).utcOffset(data.timezone_offset / 60);
|
||||
current.temperature = data.current.temp;
|
||||
current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
|
||||
current.humidity = data.current.humidity;
|
||||
if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
|
||||
current.rain = data.current["rain"]["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
|
||||
current.snow = data.current["snow"]["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
current.precipitationAmount = (current.rain ?? 0) + (current.snow ?? 0);
|
||||
}
|
||||
current.feelsLikeTemp = data.current.feels_like;
|
||||
}
|
||||
|
||||
let weather = new WeatherObject();
|
||||
|
||||
// get hourly weather, if requested
|
||||
const hours = [];
|
||||
if (data.hasOwnProperty("hourly")) {
|
||||
for (const hour of data.hourly) {
|
||||
weather.date = moment.unix(hour.dt).utcOffset(data.timezone_offset / 60);
|
||||
weather.temperature = hour.temp;
|
||||
weather.feelsLikeTemp = hour.feels_like;
|
||||
weather.humidity = hour.humidity;
|
||||
weather.windSpeed = hour.wind_speed;
|
||||
weather.windFromDirection = hour.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
|
||||
weather.precipitationProbability = hour.pop ? hour.pop * 100 : undefined;
|
||||
precip = false;
|
||||
if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
|
||||
weather.rain = hour.rain["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
|
||||
weather.snow = hour.snow["1h"];
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
}
|
||||
|
||||
hours.push(weather);
|
||||
weather = new WeatherObject();
|
||||
}
|
||||
}
|
||||
|
||||
// get daily weather, if requested
|
||||
const days = [];
|
||||
if (data.hasOwnProperty("daily")) {
|
||||
for (const day of data.daily) {
|
||||
weather.date = moment.unix(day.dt).utcOffset(data.timezone_offset / 60);
|
||||
weather.sunrise = moment.unix(day.sunrise).utcOffset(data.timezone_offset / 60);
|
||||
weather.sunset = moment.unix(day.sunset).utcOffset(data.timezone_offset / 60);
|
||||
weather.minTemperature = day.temp.min;
|
||||
weather.maxTemperature = day.temp.max;
|
||||
weather.humidity = day.humidity;
|
||||
weather.windSpeed = day.wind_speed;
|
||||
weather.windFromDirection = day.wind_deg;
|
||||
weather.weatherType = this.convertWeatherType(day.weather[0].icon);
|
||||
weather.precipitationProbability = day.pop ? day.pop * 100 : undefined;
|
||||
precip = false;
|
||||
if (!isNaN(day.rain)) {
|
||||
weather.rain = day.rain;
|
||||
precip = true;
|
||||
}
|
||||
if (!isNaN(day.snow)) {
|
||||
weather.snow = day.snow;
|
||||
precip = true;
|
||||
}
|
||||
if (precip) {
|
||||
weather.precipitationAmount = (weather.rain ?? 0) + (weather.snow ?? 0);
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
weather = new WeatherObject();
|
||||
}
|
||||
}
|
||||
|
||||
return { current: current, hours: hours, days: days };
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the OpenWeatherMap icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"01d": "day-sunny",
|
||||
"02d": "day-cloudy",
|
||||
"03d": "cloudy",
|
||||
"04d": "cloudy-windy",
|
||||
"09d": "showers",
|
||||
"10d": "rain",
|
||||
"11d": "thunderstorm",
|
||||
"13d": "snow",
|
||||
"50d": "fog",
|
||||
"01n": "night-clear",
|
||||
"02n": "night-cloudy",
|
||||
"03n": "night-cloudy",
|
||||
"04n": "night-cloudy",
|
||||
"09n": "night-showers",
|
||||
"10n": "night-rain",
|
||||
"11n": "night-thunderstorm",
|
||||
"13n": "night-snow",
|
||||
"50n": "night-alt-cloudy-windy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/* getParams(compliments)
|
||||
* Generates an url with api parameters based on the config.
|
||||
*
|
||||
* return String - URL params.
|
||||
*/
|
||||
getParams() {
|
||||
let params = "?";
|
||||
if (this.config.weatherEndpoint === "/onecall") {
|
||||
params += `lat=${this.config.lat}`;
|
||||
params += `&lon=${this.config.lon}`;
|
||||
if (this.config.type === "current") {
|
||||
params += "&exclude=minutely,hourly,daily";
|
||||
} else if (this.config.type === "hourly") {
|
||||
params += "&exclude=current,minutely,daily";
|
||||
} else if (this.config.type === "daily" || this.config.type === "forecast") {
|
||||
params += "&exclude=current,minutely,hourly";
|
||||
} else {
|
||||
params += "&exclude=minutely";
|
||||
}
|
||||
} else if (this.config.lat && this.config.lon) {
|
||||
params += `lat=${this.config.lat}&lon=${this.config.lon}`;
|
||||
} else if (this.config.locationID) {
|
||||
params += `id=${this.config.locationID}`;
|
||||
} else if (this.config.location) {
|
||||
params += `q=${this.config.location}`;
|
||||
} else if (this.firstEvent && this.firstEvent.geo) {
|
||||
params += `lat=${this.firstEvent.geo.lat}&lon=${this.firstEvent.geo.lon}`;
|
||||
} else if (this.firstEvent && this.firstEvent.location) {
|
||||
params += `q=${this.firstEvent.location}`;
|
||||
} else {
|
||||
// TODO hide doesnt exist!
|
||||
this.hide(this.config.animationSpeed, { lockString: this.identifier });
|
||||
return;
|
||||
}
|
||||
|
||||
params += "&units=metric"; // WeatherProviders should use metric internally and use the units only for when displaying data
|
||||
params += `&lang=${this.config.lang}`;
|
||||
params += `&APPID=${this.config.apiKey}`;
|
||||
|
||||
return params;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Pirate Weather
|
||||
*
|
||||
* Written by Nicholas Hubbard https://github.com/nhubbard for formerly Dark Sky Provider
|
||||
* Modified by Karsten Hassel for Pirate Weather
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Pirate Weather, it is a replacement for Dark Sky (same api).
|
||||
*/
|
||||
WeatherProvider.register("pirateweather", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "pirateweather",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
apiBase: "https://api.pirateweather.net",
|
||||
weatherEndpoint: "/forecast",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.currently || typeof data.currently.temperature === "undefined") {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.daily || !data.daily.data.length) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.daily.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}/${this.config.apiKey}/${this.config.lat},${this.config.lon}?units=si&lang=${this.config.lang}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment();
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.currently.humidity);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.currently.temperature);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.currently.windSpeed);
|
||||
currentWeather.windFromDirection = currentWeatherData.currently.windBearing;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.currently.icon);
|
||||
currentWeather.sunrise = moment.unix(currentWeatherData.daily.data[0].sunriseTime);
|
||||
currentWeather.sunset = moment.unix(currentWeatherData.daily.data[0].sunsetTime);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment.unix(forecast.time);
|
||||
weather.minTemperature = forecast.temperatureMin;
|
||||
weather.maxTemperature = forecast.temperatureMax;
|
||||
weather.weatherType = this.convertWeatherType(forecast.icon);
|
||||
weather.snow = 0;
|
||||
weather.rain = 0;
|
||||
|
||||
let precip = 0;
|
||||
if (forecast.hasOwnProperty("precipAccumulation")) {
|
||||
precip = forecast.precipAccumulation * 10;
|
||||
}
|
||||
|
||||
weather.precipitationAmount = precip;
|
||||
if (forecast.hasOwnProperty("precipType")) {
|
||||
if (forecast.precipType === "snow") {
|
||||
weather.snow = precip;
|
||||
} else {
|
||||
weather.rain = precip;
|
||||
}
|
||||
}
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Pirate Weather to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
"clear-day": "day-sunny",
|
||||
"clear-night": "night-clear",
|
||||
rain: "rain",
|
||||
snow: "snow",
|
||||
sleet: "snow",
|
||||
wind: "wind",
|
||||
fog: "fog",
|
||||
cloudy: "cloudy",
|
||||
"partly-cloudy-day": "day-cloudy",
|
||||
"partly-cloudy-night": "night-cloudy"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: SMHI
|
||||
*
|
||||
* By BuXXi https://github.com/buxxi
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for SMHI (Sweden only). Metric system is the only
|
||||
* supported unit.
|
||||
*/
|
||||
WeatherProvider.register("smhi", {
|
||||
providerName: "SMHI",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
lat: 0, // Cant have more than 6 digits
|
||||
lon: 0, // Cant have more than 6 digits
|
||||
precipitationValue: "pmedian",
|
||||
location: false
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching current weather.
|
||||
*/
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setCurrentWeather(weatherObject);
|
||||
})
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching a multi-day forecast.
|
||||
*/
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates);
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherForecast(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements method in interface for fetching hourly forecasts.
|
||||
*/
|
||||
fetchWeatherHourly() {
|
||||
this.fetchData(this.getURL())
|
||||
.then((data) => {
|
||||
const coordinates = this.resolveCoordinates(data);
|
||||
const weatherObjects = this.convertWeatherDataGroupedBy(data.timeSeries, coordinates, "hour");
|
||||
this.setFetchedLocation(this.config.location || `(${coordinates.lat},${coordinates.lon})`);
|
||||
this.setWeatherHourly(weatherObjects);
|
||||
})
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config with checks for the precipitationValue being unset or invalid
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) === -1) {
|
||||
Log.log(`invalid or not set: ${config.precipitationValue}`);
|
||||
config.precipitationValue = this.defaults.precipitationValue;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Of all the times returned find out which one is closest to the current time, should be the first if the data isn't old.
|
||||
* @param {object[]} times Array of time objects
|
||||
* @returns {object} The weatherdata closest to the current time
|
||||
*/
|
||||
getClosestToCurrentTime(times) {
|
||||
let now = moment();
|
||||
let minDiff = undefined;
|
||||
for (const time of times) {
|
||||
let diff = Math.abs(moment(time.validTime).diff(now));
|
||||
if (!minDiff || diff < Math.abs(moment(minDiff.validTime).diff(now))) {
|
||||
minDiff = time;
|
||||
}
|
||||
}
|
||||
return minDiff;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the forecast url for the configured coordinates
|
||||
* @returns {string} the url for the specified coordinates
|
||||
*/
|
||||
getURL() {
|
||||
const formatter = new Intl.NumberFormat("en-US", {
|
||||
minimumFractionDigits: 6,
|
||||
maximumFractionDigits: 6
|
||||
});
|
||||
const lon = formatter.format(this.config.lon);
|
||||
const lat = formatter.format(this.config.lat);
|
||||
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the apparent temperature based on known atmospheric data.
|
||||
* @param {object} weatherData Weatherdata to use for the calculation
|
||||
* @returns {number} The apparent temperature
|
||||
*/
|
||||
calculateApparentTemperature(weatherData) {
|
||||
const Ta = this.paramValue(weatherData, "t");
|
||||
const rh = this.paramValue(weatherData, "r");
|
||||
const ws = this.paramValue(weatherData, "ws");
|
||||
const p = (rh / 100) * 6.105 * Math.E * ((17.27 * Ta) / (237.7 + Ta));
|
||||
|
||||
return Ta + 0.33 * p - 0.7 * ws - 4;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the returned data into a WeatherObject with required properties set for both current weather and forecast.
|
||||
* The returned units is always in metric system.
|
||||
* Requires coordinates to determine if its daytime or nighttime to know which icon to use and also to set sunrise and sunset.
|
||||
* @param {object} weatherData Weatherdata to convert
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @returns {WeatherObject} The converted weatherdata at the specified location
|
||||
*/
|
||||
convertWeatherDataToObject(weatherData, coordinates) {
|
||||
let currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment(weatherData.validTime);
|
||||
currentWeather.updateSunTime(coordinates.lat, coordinates.lon);
|
||||
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||
currentWeather.windFromDirection = this.paramValue(weatherData, "wd");
|
||||
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), currentWeather.isDayTime());
|
||||
currentWeather.feelsLikeTemp = this.calculateApparentTemperature(weatherData);
|
||||
|
||||
// Determine the precipitation amount and category and update the
|
||||
// weatherObject with it, the valuetype to use can be configured or uses
|
||||
// median as default.
|
||||
let precipitationValue = this.paramValue(weatherData, this.config.precipitationValue);
|
||||
switch (this.paramValue(weatherData, "pcat")) {
|
||||
// 0 = No precipitation
|
||||
case 1: // Snow
|
||||
currentWeather.snow += precipitationValue;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||
currentWeather.snow += precipitationValue / 2;
|
||||
currentWeather.rain += precipitationValue / 2;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
case 3: // Rain
|
||||
case 4: // Drizzle
|
||||
case 5: // Freezing rain
|
||||
case 6: // Freezing drizzle
|
||||
currentWeather.rain += precipitationValue;
|
||||
currentWeather.precipitationAmount += precipitationValue;
|
||||
break;
|
||||
}
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes all the data points and converts it to one WeatherObject per day.
|
||||
* @param {object[]} allWeatherData Array of weatherdata
|
||||
* @param {object} coordinates Coordinates of the locations of the weather
|
||||
* @param {string} groupBy The interval to use for grouping the data (day, hour)
|
||||
* @returns {WeatherObject[]} Array of weatherobjects
|
||||
*/
|
||||
convertWeatherDataGroupedBy(allWeatherData, coordinates, groupBy = "day") {
|
||||
let currentWeather;
|
||||
let result = [];
|
||||
|
||||
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||
let dayWeatherTypes = [];
|
||||
|
||||
for (const weatherObject of allWeatherObjects) {
|
||||
//If its the first object or if a day/hour change we need to reset the summary object
|
||||
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, groupBy)) {
|
||||
currentWeather = new WeatherObject();
|
||||
dayWeatherTypes = [];
|
||||
currentWeather.temperature = weatherObject.temperature;
|
||||
currentWeather.date = weatherObject.date;
|
||||
currentWeather.minTemperature = Infinity;
|
||||
currentWeather.maxTemperature = -Infinity;
|
||||
currentWeather.snow = 0;
|
||||
currentWeather.rain = 0;
|
||||
currentWeather.precipitationAmount = 0;
|
||||
result.push(currentWeather);
|
||||
}
|
||||
|
||||
//Keep track of what icons have been used for each hour of daytime and use the middle one for the forecast
|
||||
if (weatherObject.isDayTime()) {
|
||||
dayWeatherTypes.push(weatherObject.weatherType);
|
||||
}
|
||||
if (dayWeatherTypes.length > 0) {
|
||||
currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
|
||||
} else {
|
||||
currentWeather.weatherType = weatherObject.weatherType;
|
||||
}
|
||||
|
||||
//All other properties is either a sum, min or max of each hour
|
||||
currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
|
||||
currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
|
||||
currentWeather.snow += weatherObject.snow;
|
||||
currentWeather.rain += weatherObject.rain;
|
||||
currentWeather.precipitationAmount += weatherObject.precipitationAmount;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Resolve coordinates from the response data (probably preferably to use
|
||||
* this if it's not matching the config values exactly)
|
||||
* @param {object} data Response data from the weather service
|
||||
* @returns {{lon, lat}} the lat/long coordinates of the data
|
||||
*/
|
||||
resolveCoordinates(data) {
|
||||
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||
},
|
||||
|
||||
/**
|
||||
* The distance between the data points is increasing in the data the more distant the prediction is.
|
||||
* Find these gaps and fill them with the previous hours data to make the data returned a complete set.
|
||||
* @param {object[]} data Response data from the weather service
|
||||
* @returns {object[]} Given data with filled gaps
|
||||
*/
|
||||
fillInGaps(data) {
|
||||
let result = [];
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
let to = moment(data[i].validTime);
|
||||
let from = moment(data[i - 1].validTime);
|
||||
let hours = moment.duration(to.diff(from)).asHours();
|
||||
// For each hour add a datapoint but change the validTime
|
||||
for (let j = 0; j < hours; j++) {
|
||||
let current = Object.assign({}, data[i]);
|
||||
current.validTime = from.clone().add(j, "hours").toISOString();
|
||||
result.push(current);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method to get a property from the returned data set.
|
||||
* @param {object} currentWeatherData Weatherdata to get from
|
||||
* @param {string} name The name of the property
|
||||
* @returns {*} The value of the property in the weatherdata
|
||||
*/
|
||||
paramValue(currentWeatherData, name) {
|
||||
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Map the icon value from SMHI to an icon that MagicMirror² understands.
|
||||
* Uses different icons depending on if its daytime or nighttime.
|
||||
* SMHI's description of what the numeric value means is the comment after the case.
|
||||
* @param {number} input The SMHI icon value
|
||||
* @param {boolean} isDayTime True if the icon should be for daytime, false for nighttime
|
||||
* @returns {string} The icon name for the MagicMirror
|
||||
*/
|
||||
convertWeatherType(input, isDayTime) {
|
||||
switch (input) {
|
||||
case 1:
|
||||
return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
|
||||
case 2:
|
||||
return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
|
||||
case 3:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable cloudiness
|
||||
case 4:
|
||||
return isDayTime ? "day-cloudy" : "night-cloudy"; // Halfclear sky
|
||||
case 5:
|
||||
return "cloudy"; // Cloudy sky
|
||||
case 6:
|
||||
return "cloudy"; // Overcast
|
||||
case 7:
|
||||
return "fog"; // Fog
|
||||
case 8:
|
||||
return "showers"; // Light rain showers
|
||||
case 9:
|
||||
return "showers"; // Moderate rain showers
|
||||
case 10:
|
||||
return "showers"; // Heavy rain showers
|
||||
case 11:
|
||||
return "thunderstorm"; // Thunderstorm
|
||||
case 12:
|
||||
return "sleet"; // Light sleet showers
|
||||
case 13:
|
||||
return "sleet"; // Moderate sleet showers
|
||||
case 14:
|
||||
return "sleet"; // Heavy sleet showers
|
||||
case 15:
|
||||
return "snow"; // Light snow showers
|
||||
case 16:
|
||||
return "snow"; // Moderate snow showers
|
||||
case 17:
|
||||
return "snow"; // Heavy snow showers
|
||||
case 18:
|
||||
return "rain"; // Light rain
|
||||
case 19:
|
||||
return "rain"; // Moderate rain
|
||||
case 20:
|
||||
return "rain"; // Heavy rain
|
||||
case 21:
|
||||
return "thunderstorm"; // Thunder
|
||||
case 22:
|
||||
return "sleet"; // Light sleet
|
||||
case 23:
|
||||
return "sleet"; // Moderate sleet
|
||||
case 24:
|
||||
return "sleet"; // Heavy sleet
|
||||
case 25:
|
||||
return "snow"; // Light snowfall
|
||||
case 26:
|
||||
return "snow"; // Moderate snowfall
|
||||
case 27:
|
||||
return "snow"; // Heavy snowfall
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Datapoint.
|
||||
*/
|
||||
WeatherProvider.register("ukmetoffice", {
|
||||
// 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: "UK Met Office",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/",
|
||||
locationID: false,
|
||||
apiKey: ""
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl("3hourly"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
|
||||
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Overwrite the fetchCurrentWeather method.
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl("daily"))
|
||||
.then((data) => {
|
||||
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location || !data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
return;
|
||||
}
|
||||
|
||||
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/** UK Met Office Specific Methods - These are not part of the default provider methods */
|
||||
/*
|
||||
* Gets the complete url for the request
|
||||
*/
|
||||
getUrl(forecastType) {
|
||||
return this.config.apiBase + this.config.locationID + this.getParams(forecastType);
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate a WeatherObject based on currentWeatherInformation
|
||||
*/
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
const location = currentWeatherData.SiteRep.DV.Location;
|
||||
|
||||
// data times are always UTC
|
||||
let nowUtc = moment.utc();
|
||||
let midnightUtc = nowUtc.clone().startOf("day");
|
||||
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
|
||||
|
||||
// loop round each of the (5) periods, look for today (the first period may be yesterday)
|
||||
for (const period of location.Period) {
|
||||
const periodDate = moment.utc(period.value.substr(0, 10), "YYYY-MM-DD");
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// check this is the period we want, after today the diff will be -ve
|
||||
if (moment().diff(periodDate, "minutes") > 0) {
|
||||
// loop round the reports looking for the one we are in
|
||||
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
|
||||
for (const rep of period.Rep) {
|
||||
const p = rep.$;
|
||||
if (timeInMins >= p && timeInMins - 180 < p) {
|
||||
// finally got the one we want, so populate weather object
|
||||
currentWeather.humidity = rep.H;
|
||||
currentWeather.temperature = rep.T;
|
||||
currentWeather.feelsLikeTemp = rep.F;
|
||||
currentWeather.precipitationProbability = parseInt(rep.Pp);
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMetric(rep.S);
|
||||
currentWeather.windFromDirection = WeatherUtils.convertWindDirection(rep.D);
|
||||
currentWeather.weatherType = this.convertWeatherType(rep.W);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// determine the sunrise/sunset times - not supplied in UK Met Office data
|
||||
currentWeather.updateSunTime(location.lat, location.lon);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
/*
|
||||
* Generate WeatherObjects based on forecast information
|
||||
*/
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
// loop round the (5) periods getting the data
|
||||
// for each period array, Day is [0], Night is [1]
|
||||
for (const period of forecasts.SiteRep.DV.Location.Period) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
// data times are always UTC
|
||||
const dateStr = period.value;
|
||||
let periodDate = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
||||
|
||||
// ignore if period is before today
|
||||
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
|
||||
// populate the weather object
|
||||
weather.date = moment.utc(dateStr.substr(0, 10), "YYYY-MM-DD");
|
||||
weather.minTemperature = period.Rep[1].Nm;
|
||||
weather.maxTemperature = period.Rep[0].Dm;
|
||||
weather.weatherType = this.convertWeatherType(period.Rep[0].W);
|
||||
weather.precipitationProbability = parseInt(period.Rep[0].PPd);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
/*
|
||||
* Convert the Met Office icons to a more usable name.
|
||||
*/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
2: "night-alt-cloudy",
|
||||
3: "day-cloudy",
|
||||
5: "fog",
|
||||
6: "fog",
|
||||
7: "cloudy",
|
||||
8: "cloud",
|
||||
9: "night-sprinkle",
|
||||
10: "day-sprinkle",
|
||||
11: "raindrops",
|
||||
12: "sprinkle",
|
||||
13: "night-alt-showers",
|
||||
14: "day-showers",
|
||||
15: "rain",
|
||||
16: "night-alt-sleet",
|
||||
17: "day-sleet",
|
||||
18: "sleet",
|
||||
19: "night-alt-hail",
|
||||
20: "day-hail",
|
||||
21: "hail",
|
||||
22: "night-alt-snow",
|
||||
23: "day-snow",
|
||||
24: "snow",
|
||||
25: "night-alt-snow",
|
||||
26: "day-snow",
|
||||
27: "snow",
|
||||
28: "night-alt-thunderstorm",
|
||||
29: "day-thunderstorm",
|
||||
30: "thunderstorm"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates an url with api parameters based on the config.
|
||||
* @param {string} forecastType daily or 3hourly forecast
|
||||
* @returns {string} url
|
||||
*/
|
||||
getParams(forecastType) {
|
||||
let params = "?";
|
||||
params += `res=${forecastType}`;
|
||||
params += `&key=${this.config.apiKey}`;
|
||||
return params;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
*
|
||||
* By Malcolm Oakes https://github.com/maloakes
|
||||
* Existing Met Office provider edited for new MetOffice Data Hub by CreepinJesus http://github.com/XBCreepinJesus
|
||||
* MIT Licensed.
|
||||
*
|
||||
* This class is a provider for UK Met Office Data Hub (the replacement for their Data Point services).
|
||||
* For more information on Data Hub, see https://www.metoffice.gov.uk/services/data/datapoint/notifications/weather-datahub
|
||||
* Data available:
|
||||
* Hourly data for next 2 days ("hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-hourly.pdf
|
||||
* 3-hourly data for the next 7 days ("3hourly") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-3-hourly.pdf
|
||||
* Daily data for the next 7 days ("daily") - https://www.metoffice.gov.uk/binaries/content/assets/metofficegovuk/pdf/data/global-spot-data-daily.pdf
|
||||
*
|
||||
* NOTES
|
||||
* This provider requires longitude/latitude coordinates, rather than a location ID (as with the previous Met Office provider)
|
||||
* Provide the following in your config.js file:
|
||||
* weatherProvider: "ukmetofficedatahub",
|
||||
* apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
* apiKey: "[YOUR API KEY]",
|
||||
* apiSecret: "[YOUR API SECRET]",
|
||||
* lat: [LATITUDE (DECIMAL)],
|
||||
* lon: [LONGITUDE (DECIMAL)]
|
||||
*
|
||||
* At time of writing, free accounts are limited to 360 requests a day per service (hourly, 3hourly, daily); take this in mind when
|
||||
* setting your update intervals. For reference, 360 requests per day is once every 4 minutes.
|
||||
*
|
||||
* Pay attention to the units of the supplied data from the Met Office - it is given in SI/metric units where applicable:
|
||||
* - Temperatures are in degrees Celsius (°C)
|
||||
* - Wind speeds are in metres per second (m/s)
|
||||
* - Wind direction given in degrees (°)
|
||||
* - Pressures are in Pascals (Pa)
|
||||
* - Distances are in metres (m)
|
||||
* - Probabilities and humidity are given as percentages (%)
|
||||
* - Precipitation is measured in millimetres (mm) with rates per hour (mm/h)
|
||||
*
|
||||
* See the PDFs linked above for more information on the data their corresponding units.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("ukmetofficedatahub", {
|
||||
// Set the name of the provider.
|
||||
providerName: "UK Met Office (DataHub)",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api-metoffice.apiconnect.ibmcloud.com/metoffice/production/v0/forecasts/point/",
|
||||
apiKey: "",
|
||||
apiSecret: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
// Build URL with query strings according to DataHub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getUrl(forecastType) {
|
||||
let queryStrings = "?";
|
||||
queryStrings += `latitude=${this.config.lat}`;
|
||||
queryStrings += `&longitude=${this.config.lon}`;
|
||||
queryStrings += `&includeLocationName=${true}`;
|
||||
|
||||
// Return URL, making sure there is a trailing "/" in the base URL.
|
||||
return this.config.apiBase + (this.config.apiBase.endsWith("/") ? "" : "/") + forecastType + queryStrings;
|
||||
},
|
||||
|
||||
// Build the list of headers for the request
|
||||
// For DataHub requests, the API key/secret are sent in the headers rather than as query strings.
|
||||
// Headers defined according to Data Hub API (https://metoffice.apiconnect.ibmcloud.com/metoffice/production/api)
|
||||
getHeaders() {
|
||||
return {
|
||||
accept: "application/json",
|
||||
"x-ibm-client-id": this.config.apiKey,
|
||||
"x-ibm-client-secret": this.config.apiSecret
|
||||
};
|
||||
},
|
||||
|
||||
// Fetch data using supplied URL and request headers
|
||||
async fetchWeather(url, headers) {
|
||||
const response = await fetch(url, { headers: headers });
|
||||
|
||||
// Return JSON data
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Fetch hourly forecast data (to use for current weather)
|
||||
fetchCurrentWeather() {
|
||||
this.fetchWeather(this.getUrl("hourly"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad current/hourly data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set location name
|
||||
this.setFetchedLocation(`${data.features[0].properties.location.name}`);
|
||||
|
||||
// Generate current weather data
|
||||
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
|
||||
// Let the module know there is data available
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a WeatherObject using current weather data (data for the current hour)
|
||||
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataHours = currentWeatherData.features[0].properties.timeSeries;
|
||||
|
||||
// Define now
|
||||
let nowUtc = moment.utc();
|
||||
|
||||
// Find hour that contains the current time
|
||||
for (let hour in forecastDataHours) {
|
||||
let forecastTime = moment.utc(forecastDataHours[hour].time);
|
||||
if (nowUtc.isSameOrAfter(forecastTime) && nowUtc.isBefore(moment(forecastTime.add(1, "h")))) {
|
||||
currentWeather.date = forecastTime;
|
||||
currentWeather.windSpeed = forecastDataHours[hour].windSpeed10m;
|
||||
currentWeather.windFromDirection = forecastDataHours[hour].windDirectionFrom10m;
|
||||
currentWeather.temperature = forecastDataHours[hour].screenTemperature;
|
||||
currentWeather.minTemperature = forecastDataHours[hour].minScreenAirTemp;
|
||||
currentWeather.maxTemperature = forecastDataHours[hour].maxScreenAirTemp;
|
||||
currentWeather.weatherType = this.convertWeatherType(forecastDataHours[hour].significantWeatherCode);
|
||||
currentWeather.humidity = forecastDataHours[hour].screenRelativeHumidity;
|
||||
currentWeather.rain = forecastDataHours[hour].totalPrecipAmount;
|
||||
currentWeather.snow = forecastDataHours[hour].totalSnowAmount;
|
||||
currentWeather.precipitationProbability = forecastDataHours[hour].probOfPrecipitation;
|
||||
currentWeather.feelsLikeTemp = forecastDataHours[hour].feelsLikeTemperature;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
currentWeather.rawData = forecastDataHours[hour];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the sunrise/sunset times - (still) not supplied in UK Met Office data
|
||||
// Passes {longitude, latitude} to SunCalc, could pass height to, but
|
||||
// SunCalc.getTimes doesn't take that into account
|
||||
currentWeather.updateSunTime(this.config.lat, this.config.lon);
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
// Fetch daily forecast data
|
||||
fetchWeatherForecast() {
|
||||
this.fetchWeather(this.getUrl("daily"), this.getHeaders())
|
||||
.then((data) => {
|
||||
// Check data is usable
|
||||
if (!data || !data.features || !data.features[0].properties || !data.features[0].properties.timeSeries || data.features[0].properties.timeSeries.length === 0) {
|
||||
// Did not receive usable new data.
|
||||
// Maybe this needs a better check?
|
||||
Log.error("Possibly bad forecast data?");
|
||||
Log.error(data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set location name
|
||||
this.setFetchedLocation(`${data.features[0].properties.location.name}`);
|
||||
|
||||
// Generate the forecast data
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data);
|
||||
this.setWeatherForecast(forecast);
|
||||
})
|
||||
|
||||
// Catch any error(s)
|
||||
.catch((error) => Log.error(`Could not load data: ${error.message}`))
|
||||
|
||||
// Let the module know there is new data available
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a WeatherObject for each day using daily forecast data
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const dailyForecasts = [];
|
||||
|
||||
// Extract the actual forecasts
|
||||
let forecastDataDays = forecasts.features[0].properties.timeSeries;
|
||||
|
||||
// Define today
|
||||
let today = moment.utc().startOf("date");
|
||||
|
||||
// Go through each day in the forecasts
|
||||
for (let day in forecastDataDays) {
|
||||
const forecastWeather = new WeatherObject();
|
||||
|
||||
// Get date of forecast
|
||||
let forecastDate = moment.utc(forecastDataDays[day].time);
|
||||
|
||||
// Check if forecast is for today or in the future (i.e., ignore yesterday's forecast)
|
||||
if (forecastDate.isSameOrAfter(today)) {
|
||||
forecastWeather.date = forecastDate;
|
||||
forecastWeather.minTemperature = forecastDataDays[day].nightMinScreenTemperature;
|
||||
forecastWeather.maxTemperature = forecastDataDays[day].dayMaxScreenTemperature;
|
||||
|
||||
// Using daytime forecast values
|
||||
forecastWeather.windSpeed = forecastDataDays[day].midday10MWindSpeed;
|
||||
forecastWeather.windFromDirection = forecastDataDays[day].midday10MWindDirection;
|
||||
forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
|
||||
forecastWeather.precipitationProbability = forecastDataDays[day].dayProbabilityOfPrecipitation;
|
||||
forecastWeather.temperature = forecastDataDays[day].dayMaxScreenTemperature;
|
||||
forecastWeather.humidity = forecastDataDays[day].middayRelativeHumidity;
|
||||
forecastWeather.rain = forecastDataDays[day].dayProbabilityOfRain;
|
||||
forecastWeather.snow = forecastDataDays[day].dayProbabilityOfSnow;
|
||||
forecastWeather.feelsLikeTemp = forecastDataDays[day].dayMaxFeelsLikeTemp;
|
||||
|
||||
// Pass on full details, so they can be used in custom templates
|
||||
// Note the units of the supplied data when using this (see top of file)
|
||||
forecastWeather.rawData = forecastDataDays[day];
|
||||
|
||||
dailyForecasts.push(forecastWeather);
|
||||
}
|
||||
}
|
||||
|
||||
return dailyForecasts;
|
||||
},
|
||||
|
||||
// Set the fetched location name.
|
||||
setFetchedLocation: function (name) {
|
||||
this.fetchedLocationName = name;
|
||||
},
|
||||
|
||||
// Match the Met Office "significant weather code" to a weathericons.css icon
|
||||
// Use: https://metoffice.apiconnect.ibmcloud.com/metoffice/production/node/264
|
||||
// and: https://erikflowers.github.io/weather-icons/
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
0: "night-clear",
|
||||
1: "day-sunny",
|
||||
2: "night-alt-cloudy",
|
||||
3: "day-cloudy",
|
||||
5: "fog",
|
||||
6: "fog",
|
||||
7: "cloudy",
|
||||
8: "cloud",
|
||||
9: "night-sprinkle",
|
||||
10: "day-sprinkle",
|
||||
11: "raindrops",
|
||||
12: "sprinkle",
|
||||
13: "night-alt-showers",
|
||||
14: "day-showers",
|
||||
15: "rain",
|
||||
16: "night-alt-sleet",
|
||||
17: "day-sleet",
|
||||
18: "sleet",
|
||||
19: "night-alt-hail",
|
||||
20: "day-hail",
|
||||
21: "hail",
|
||||
22: "night-alt-snow",
|
||||
23: "day-snow",
|
||||
24: "snow",
|
||||
25: "night-alt-snow",
|
||||
26: "day-snow",
|
||||
27: "snow",
|
||||
28: "night-alt-thunderstorm",
|
||||
29: "day-thunderstorm",
|
||||
30: "thunderstorm"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Weatherbit
|
||||
*
|
||||
* By Andrew Pometti
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherbit, based on Nicholas Hubbard's class
|
||||
* for Dark Sky & Vince Peri's class for Weather.gov.
|
||||
*/
|
||||
WeatherProvider.register("weatherbit", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging.
|
||||
providerName: "Weatherbit",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://api.weatherbit.io/v2.0",
|
||||
apiKey: "",
|
||||
lat: 0,
|
||||
lon: 0
|
||||
},
|
||||
|
||||
fetchedLocation: function () {
|
||||
return this.fetchedLocationName || "";
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data[0] || typeof data.data[0].temp === "undefined") {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const currentWeather = this.generateWeatherDayFromCurrentWeather(data);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
if (!data || !data.data) {
|
||||
// No usable data?
|
||||
return;
|
||||
}
|
||||
|
||||
const forecast = this.generateWeatherObjectsFromForecast(data.data);
|
||||
this.setWeatherForecast(forecast);
|
||||
|
||||
this.fetchedLocationName = `${data.city_name}, ${data.state_code}`;
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides method for setting config to check if endpoint is correct for hourly
|
||||
* @param {object} config The configuration object
|
||||
*/
|
||||
setConfig(config) {
|
||||
this.config = config;
|
||||
if (!this.config.weatherEndpoint) {
|
||||
switch (this.config.type) {
|
||||
case "hourly":
|
||||
this.config.weatherEndpoint = "/forecast/hourly";
|
||||
break;
|
||||
case "daily":
|
||||
case "forecast":
|
||||
this.config.weatherEndpoint = "/forecast/daily";
|
||||
break;
|
||||
case "current":
|
||||
this.config.weatherEndpoint = "/current";
|
||||
break;
|
||||
default:
|
||||
Log.error("weatherEndpoint not configured and could not resolve it based on type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
return `${this.config.apiBase}${this.config.weatherEndpoint}?lat=${this.config.lat}&lon=${this.config.lon}&units=M&key=${this.config.apiKey}`;
|
||||
},
|
||||
|
||||
// Implement WeatherDay generator.
|
||||
generateWeatherDayFromCurrentWeather(currentWeatherData) {
|
||||
//Calculate TZ Offset and invert to convert Sunrise/Sunset times to Local
|
||||
const d = new Date();
|
||||
let tzOffset = d.getTimezoneOffset();
|
||||
tzOffset = tzOffset * -1;
|
||||
|
||||
const currentWeather = new WeatherObject();
|
||||
|
||||
currentWeather.date = moment.unix(currentWeatherData.data[0].ts);
|
||||
currentWeather.humidity = parseFloat(currentWeatherData.data[0].rh);
|
||||
currentWeather.temperature = parseFloat(currentWeatherData.data[0].temp);
|
||||
currentWeather.windSpeed = parseFloat(currentWeatherData.data[0].wind_spd);
|
||||
currentWeather.windFromDirection = currentWeatherData.data[0].wind_dir;
|
||||
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.data[0].weather.icon);
|
||||
currentWeather.sunrise = moment(currentWeatherData.data[0].sunrise, "HH:mm").add(tzOffset, "m");
|
||||
currentWeather.sunset = moment(currentWeatherData.data[0].sunset, "HH:mm").add(tzOffset, "m");
|
||||
|
||||
this.fetchedLocationName = `${currentWeatherData.data[0].city_name}, ${currentWeatherData.data[0].state_code}`;
|
||||
|
||||
return currentWeather;
|
||||
},
|
||||
|
||||
generateWeatherObjectsFromForecast(forecasts) {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of forecasts) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment(forecast.datetime, "YYYY-MM-DD");
|
||||
weather.minTemperature = forecast.min_temp;
|
||||
weather.maxTemperature = forecast.max_temp;
|
||||
weather.precipitationAmount = forecast.precip;
|
||||
weather.precipitationProbability = forecast.pop;
|
||||
weather.weatherType = this.convertWeatherType(forecast.weather.icon);
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
return days;
|
||||
},
|
||||
|
||||
// Map icons from Dark Sky to our icons.
|
||||
convertWeatherType(weatherType) {
|
||||
const weatherTypes = {
|
||||
t01d: "day-thunderstorm",
|
||||
t01n: "night-alt-thunderstorm",
|
||||
t02d: "day-thunderstorm",
|
||||
t02n: "night-alt-thunderstorm",
|
||||
t03d: "thunderstorm",
|
||||
t03n: "thunderstorm",
|
||||
t04d: "day-thunderstorm",
|
||||
t04n: "night-alt-thunderstorm",
|
||||
t05d: "day-sleet-storm",
|
||||
t05n: "night-alt-sleet-storm",
|
||||
d01d: "day-sprinkle",
|
||||
d01n: "night-alt-sprinkle",
|
||||
d02d: "day-sprinkle",
|
||||
d02n: "night-alt-sprinkle",
|
||||
d03d: "day-shower",
|
||||
d03n: "night-alt-shower",
|
||||
r01d: "day-shower",
|
||||
r01n: "night-alt-shower",
|
||||
r02d: "day-rain",
|
||||
r02n: "night-alt-rain",
|
||||
r03d: "day-rain",
|
||||
r03n: "night-alt-rain",
|
||||
r04d: "day-sprinkle",
|
||||
r04n: "night-alt-sprinkle",
|
||||
r05d: "day-shower",
|
||||
r05n: "night-alt-shower",
|
||||
r06d: "day-shower",
|
||||
r06n: "night-alt-shower",
|
||||
f01d: "day-sleet",
|
||||
f01n: "night-alt-sleet",
|
||||
s01d: "day-snow",
|
||||
s01n: "night-alt-snow",
|
||||
s02d: "day-snow-wind",
|
||||
s02n: "night-alt-snow-wind",
|
||||
s03d: "snowflake-cold",
|
||||
s03n: "snowflake-cold",
|
||||
s04d: "day-rain-mix",
|
||||
s04n: "night-alt-rain-mix",
|
||||
s05d: "day-sleet",
|
||||
s05n: "night-alt-sleet",
|
||||
s06d: "day-snow",
|
||||
s06n: "night-alt-snow",
|
||||
a01d: "day-haze",
|
||||
a01n: "dust",
|
||||
a02d: "smoke",
|
||||
a02n: "smoke",
|
||||
a03d: "day-haze",
|
||||
a03n: "dust",
|
||||
a04d: "dust",
|
||||
a04n: "dust",
|
||||
a05d: "day-fog",
|
||||
a05n: "night-fog",
|
||||
a06d: "fog",
|
||||
a06n: "fog",
|
||||
c01d: "day-sunny",
|
||||
c01n: "night-clear",
|
||||
c02d: "day-sunny-overcast",
|
||||
c02n: "night-alt-partly-cloudy",
|
||||
c03d: "day-cloudy",
|
||||
c03n: "night-alt-cloudy",
|
||||
c04d: "cloudy",
|
||||
c04n: "cloudy",
|
||||
u00d: "rain-mix",
|
||||
u00n: "rain-mix"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
/* global WeatherProvider, WeatherObject, WeatherUtils */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Weatherflow
|
||||
*
|
||||
* By Tobias Dreyem https://github.com/10bias
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Weatherflow.
|
||||
* Note that the Weatherflow API does not provide snowfall.
|
||||
*/
|
||||
|
||||
WeatherProvider.register("weatherflow", {
|
||||
// Set the name of the provider.
|
||||
// Not strictly required, but helps for debugging
|
||||
providerName: "WeatherFlow",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
apiBase: "https://swd.weatherflow.com/swd/rest/",
|
||||
token: "",
|
||||
stationid: ""
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
const currentWeather = new WeatherObject();
|
||||
currentWeather.date = moment();
|
||||
|
||||
currentWeather.humidity = data.current_conditions.relative_humidity;
|
||||
currentWeather.temperature = data.current_conditions.air_temperature;
|
||||
currentWeather.windSpeed = WeatherUtils.convertWindToMs(data.current_conditions.wind_avg);
|
||||
currentWeather.windFromDirection = data.current_conditions.wind_direction;
|
||||
currentWeather.weatherType = data.forecast.daily[0].icon;
|
||||
currentWeather.sunrise = moment.unix(data.forecast.daily[0].sunrise);
|
||||
currentWeather.sunset = moment.unix(data.forecast.daily[0].sunset);
|
||||
this.setCurrentWeather(currentWeather);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.fetchData(this.getUrl())
|
||||
.then((data) => {
|
||||
const days = [];
|
||||
|
||||
for (const forecast of data.forecast.daily) {
|
||||
const weather = new WeatherObject();
|
||||
|
||||
weather.date = moment.unix(forecast.day_start_local);
|
||||
weather.minTemperature = forecast.air_temp_low;
|
||||
weather.maxTemperature = forecast.air_temp_high;
|
||||
weather.precipitationProbability = forecast.precip_probability;
|
||||
weather.weatherType = forecast.icon;
|
||||
weather.snow = 0;
|
||||
|
||||
days.push(weather);
|
||||
}
|
||||
|
||||
this.setWeatherForecast(days);
|
||||
})
|
||||
.catch(function (request) {
|
||||
Log.error("Could not load data ... ", request);
|
||||
})
|
||||
.finally(() => this.updateAvailable());
|
||||
},
|
||||
|
||||
// Create a URL from the config and base URL.
|
||||
getUrl() {
|
||||
return `${this.config.apiBase}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}`;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,367 @@
|
||||
/* 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;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,632 @@
|
||||
/* global WeatherProvider, WeatherObject */
|
||||
|
||||
/* MagicMirror²
|
||||
* Module: Weather
|
||||
* Provider: Yr.no
|
||||
*
|
||||
* By Magnus Marthinsen
|
||||
* MIT Licensed
|
||||
*
|
||||
* This class is a provider for Yr.no, a norwegian weather service.
|
||||
*
|
||||
* Terms of service: https://developer.yr.no/doc/TermsOfService/
|
||||
*/
|
||||
WeatherProvider.register("yr", {
|
||||
providerName: "Yr",
|
||||
|
||||
// Set the default config properties that is specific to this provider
|
||||
defaults: {
|
||||
useCorsProxy: true,
|
||||
apiBase: "https://api.met.no/weatherapi",
|
||||
altitude: 0,
|
||||
currentForecastHours: 1 //1, 6 or 12
|
||||
},
|
||||
|
||||
start() {
|
||||
if (typeof Storage === "undefined") {
|
||||
//local storage unavailable
|
||||
Log.error("The Yr weather provider requires local storage.");
|
||||
throw new Error("Local storage not available");
|
||||
}
|
||||
Log.info(`Weather provider: ${this.providerName} started.`);
|
||||
},
|
||||
|
||||
fetchCurrentWeather() {
|
||||
this.getCurrentWeather()
|
||||
.then((currentWeather) => {
|
||||
this.setCurrentWeather(currentWeather);
|
||||
this.updateAvailable();
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
});
|
||||
},
|
||||
|
||||
async getCurrentWeather() {
|
||||
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||
if (!stellarData) {
|
||||
Log.warn("No stellar data available.");
|
||||
}
|
||||
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||
Log.error("No weather data available.");
|
||||
return;
|
||||
}
|
||||
const currentTime = moment();
|
||||
let forecast = weatherData.properties.timeseries[0];
|
||||
let closestTimeInPast = currentTime.diff(moment(forecast.time));
|
||||
for (const forecastTime of weatherData.properties.timeseries) {
|
||||
const comparison = currentTime.diff(moment(forecastTime.time));
|
||||
if (0 < comparison && comparison < closestTimeInPast) {
|
||||
closestTimeInPast = comparison;
|
||||
forecast = forecastTime;
|
||||
}
|
||||
}
|
||||
const forecastXHours = this.getForecastForXHoursFrom(forecast.data);
|
||||
forecast.weatherType = this.convertWeatherType(forecastXHours.summary.symbol_code, forecast.time);
|
||||
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount;
|
||||
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = forecastXHours.details?.air_temperature_min;
|
||||
forecast.maxTemperature = forecastXHours.details?.air_temperature_max;
|
||||
return this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units);
|
||||
},
|
||||
|
||||
getWeatherData() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||
if (shouldWait) {
|
||||
const checkForGo = setInterval(function () {
|
||||
shouldWait = localStorage.getItem("yrIsFetchingWeatherData");
|
||||
}, 100);
|
||||
setTimeout(function () {
|
||||
clearInterval(checkForGo);
|
||||
shouldWait = false;
|
||||
}, 5000); //Assume other fetch finished but failed to remove lock
|
||||
const attemptFetchWeather = setInterval(() => {
|
||||
if (!shouldWait) {
|
||||
clearInterval(checkForGo);
|
||||
clearInterval(attemptFetchWeather);
|
||||
this.getWeatherDataFromYrOrCache(resolve, reject);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.getWeatherDataFromYrOrCache(resolve, reject);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getWeatherDataFromYrOrCache(resolve, reject) {
|
||||
localStorage.setItem("yrIsFetchingWeatherData", "true");
|
||||
|
||||
let weatherData = this.getWeatherDataFromCache();
|
||||
if (this.weatherDataIsValid(weatherData)) {
|
||||
localStorage.removeItem("yrIsFetchingWeatherData");
|
||||
Log.debug("Weather data found in cache.");
|
||||
resolve(weatherData);
|
||||
} else {
|
||||
this.getWeatherDataFromYr(weatherData?.downloadedAt)
|
||||
.then((weatherData) => {
|
||||
Log.debug("Got weather data from yr.");
|
||||
let data;
|
||||
if (weatherData) {
|
||||
this.cacheWeatherData(weatherData);
|
||||
data = weatherData;
|
||||
} else {
|
||||
//Undefined if unchanged
|
||||
data = this.getWeatherDataFromCache();
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
reject("Unable to get weather data from Yr.");
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.removeItem("yrIsFetchingWeatherData");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
weatherDataIsValid(weatherData) {
|
||||
return (
|
||||
weatherData &&
|
||||
weatherData.timeout &&
|
||||
0 < moment(weatherData.timeout).diff(moment()) &&
|
||||
(!weatherData.geometry || !weatherData.geometry.coordinates || !weatherData.geometry.coordinates.length < 2 || (weatherData.geometry.coordinates[0] === this.config.lat && weatherData.geometry.coordinates[1] === this.config.lon))
|
||||
);
|
||||
},
|
||||
|
||||
getWeatherDataFromCache() {
|
||||
const weatherData = localStorage.getItem("weatherData");
|
||||
if (weatherData) {
|
||||
return JSON.parse(weatherData);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
getWeatherDataFromYr(currentDataFetchedAt) {
|
||||
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||
if (currentDataFetchedAt) {
|
||||
requestHeaders.push({ name: "If-Modified-Since", value: currentDataFetchedAt });
|
||||
}
|
||||
|
||||
const expectedResponseHeaders = ["expires", "date"];
|
||||
|
||||
return this.fetchData(this.getForecastUrl(), "json", requestHeaders, expectedResponseHeaders)
|
||||
.then((data) => {
|
||||
if (!data || !data.headers) return data;
|
||||
data.timeout = data.headers.find((header) => header.name === "expires").value;
|
||||
data.downloadedAt = data.headers.find((header) => header.name === "date").value;
|
||||
data.headers = undefined;
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error("Could not load weather data.", err);
|
||||
throw new Error(err);
|
||||
});
|
||||
},
|
||||
|
||||
getForecastUrl() {
|
||||
if (!this.config.lat) {
|
||||
Log.error("Latitude not provided.");
|
||||
throw new Error("Latitude not provided.");
|
||||
}
|
||||
if (!this.config.lon) {
|
||||
Log.error("Longitude not provided.");
|
||||
throw new Error("Longitude not provided.");
|
||||
}
|
||||
|
||||
let lat = this.config.lat.toString();
|
||||
let lon = this.config.lon.toString();
|
||||
const altitude = this.config.altitude ?? 0;
|
||||
|
||||
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||
Log.warn("Latitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
const latParts = lat.split(".");
|
||||
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
||||
}
|
||||
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
||||
Log.warn("Longitude is too specific for weather data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
const lonParts = lon.split(".");
|
||||
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
||||
}
|
||||
|
||||
return `${this.config.apiBase}/locationforecast/2.0/complete?&altitude=${altitude}&lat=${lat}&lon=${lon}`;
|
||||
},
|
||||
|
||||
cacheWeatherData(weatherData) {
|
||||
localStorage.setItem("weatherData", JSON.stringify(weatherData));
|
||||
},
|
||||
|
||||
getAuthenticationString() {
|
||||
if (!this.config.authenticationEmail) throw new Error("Authentication email not provided.");
|
||||
return `${this.config.applicaitionName} ${this.config.authenticationEmail}`;
|
||||
},
|
||||
|
||||
getStellarData() {
|
||||
// If a user has several Yr-modules, for instance one current and one forecast, the API calls must be synchronized across classes.
|
||||
// This is to avoid multiple similar calls to the API.
|
||||
return new Promise((resolve, reject) => {
|
||||
let shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||
if (shouldWait) {
|
||||
const checkForGo = setInterval(function () {
|
||||
shouldWait = localStorage.getItem("yrIsFetchingStellarData");
|
||||
}, 100);
|
||||
setTimeout(function () {
|
||||
clearInterval(checkForGo);
|
||||
shouldWait = false;
|
||||
}, 5000); //Assume other fetch finished but failed to remove lock
|
||||
const attemptFetchWeather = setInterval(() => {
|
||||
if (!shouldWait) {
|
||||
clearInterval(checkForGo);
|
||||
clearInterval(attemptFetchWeather);
|
||||
this.getStellarDataFromYrOrCache(resolve, reject);
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
this.getStellarDataFromYrOrCache(resolve, reject);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getStellarDataFromYrOrCache(resolve, reject) {
|
||||
localStorage.setItem("yrIsFetchingStellarData", "true");
|
||||
|
||||
let stellarData = this.getStellarDataFromCache();
|
||||
const today = moment().format("YYYY-MM-DD");
|
||||
const tomorrow = moment().add(1, "days").format("YYYY-MM-DD");
|
||||
if (stellarData && stellarData.today && stellarData.today.date === today && stellarData.tomorrow && stellarData.tomorrow.date === tomorrow) {
|
||||
Log.debug("Stellar data found in cache.");
|
||||
localStorage.removeItem("yrIsFetchingStellarData");
|
||||
resolve(stellarData);
|
||||
} else if (stellarData && stellarData.tomorrow && stellarData.tomorrow.date === today) {
|
||||
Log.debug("stellar data for today found in cache, but not for tomorrow.");
|
||||
stellarData.today = stellarData.tomorrow;
|
||||
this.getStellarDataFromYr(tomorrow)
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
data.date = tomorrow;
|
||||
stellarData.tomorrow = data;
|
||||
this.cacheStellarData(stellarData);
|
||||
resolve(stellarData);
|
||||
} else {
|
||||
reject(`No stellar data returned from Yr for ${tomorrow}`);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
reject(`Unable to get stellar data from Yr for ${tomorrow}`);
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.removeItem("yrIsFetchingStellarData");
|
||||
});
|
||||
} else {
|
||||
this.getStellarDataFromYr(today, 2)
|
||||
.then((stellarData) => {
|
||||
if (stellarData) {
|
||||
const data = {
|
||||
today: stellarData
|
||||
};
|
||||
data.tomorrow = Object.assign({}, data.today);
|
||||
data.today.date = today;
|
||||
data.tomorrow.date = tomorrow;
|
||||
this.cacheStellarData(data);
|
||||
resolve(data);
|
||||
} else {
|
||||
Log.error(`Something went wrong when fetching stellar data. Responses: ${stellarData}`);
|
||||
reject(stellarData);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error(err);
|
||||
reject("Unable to get stellar data from Yr.");
|
||||
})
|
||||
.finally(() => {
|
||||
localStorage.removeItem("yrIsFetchingStellarData");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getStellarDataFromCache() {
|
||||
const stellarData = localStorage.getItem("stellarData");
|
||||
if (stellarData) {
|
||||
return JSON.parse(stellarData);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
getStellarDataFromYr(date, days = 1) {
|
||||
const requestHeaders = [{ name: "Accept", value: "application/json" }];
|
||||
return this.fetchData(this.getStellarDatatUrl(date, days), "json", requestHeaders)
|
||||
.then((data) => {
|
||||
Log.debug("Got stellar data from yr.");
|
||||
return data;
|
||||
})
|
||||
.catch((err) => {
|
||||
Log.error("Could not load weather data.", err);
|
||||
throw new Error(err);
|
||||
});
|
||||
},
|
||||
|
||||
getStellarDatatUrl(date, days) {
|
||||
if (!this.config.lat) {
|
||||
Log.error("Latitude not provided.");
|
||||
throw new Error("Latitude not provided.");
|
||||
}
|
||||
if (!this.config.lon) {
|
||||
Log.error("Longitude not provided.");
|
||||
throw new Error("Longitude not provided.");
|
||||
}
|
||||
|
||||
let lat = this.config.lat.toString();
|
||||
let lon = this.config.lon.toString();
|
||||
const altitude = this.config.altitude ?? 0;
|
||||
|
||||
if (lat.includes(".") && lat.split(".")[1].length > 4) {
|
||||
Log.warn("Latitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
const latParts = lat.split(".");
|
||||
lat = `${latParts[0]}.${latParts[1].substring(0, 4)}`;
|
||||
}
|
||||
if (lon.includes(".") && lon.split(".")[1].length > 4) {
|
||||
Log.warn("Longitude is too specific for stellar data. Do not use more than four decimals. Trimming to maximum length.");
|
||||
const lonParts = lon.split(".");
|
||||
lon = `${lonParts[0]}.${lonParts[1].substring(0, 4)}`;
|
||||
}
|
||||
|
||||
let utcOffset = moment().utcOffset() / 60;
|
||||
let utcOffsetPrefix = "%2B";
|
||||
if (utcOffset < 0) {
|
||||
utcOffsetPrefix = "-";
|
||||
}
|
||||
utcOffset = Math.abs(utcOffset);
|
||||
let minutes = "00";
|
||||
if (utcOffset % 1 !== 0) {
|
||||
minutes = "30";
|
||||
}
|
||||
let hours = Math.floor(utcOffset).toString();
|
||||
if (hours.length < 2) {
|
||||
hours = `0${hours}`;
|
||||
}
|
||||
|
||||
return `${this.config.apiBase}/sunrise/2.0/.json?date=${date}&days=${days}&height=${altitude}&lat=${lat}&lon=${lon}&offset=${utcOffsetPrefix}${hours}%3A${minutes}`;
|
||||
},
|
||||
|
||||
cacheStellarData(data) {
|
||||
localStorage.setItem("stellarData", JSON.stringify(data));
|
||||
},
|
||||
|
||||
getWeatherDataFrom(forecast, stellarData, units) {
|
||||
const weather = new WeatherObject();
|
||||
const stellarTimesToday = stellarData?.today ? this.getStellarTimesFrom(stellarData.today, moment().format("YYYY-MM-DD")) : undefined;
|
||||
const stellarTimesTomorrow = stellarData?.tomorrow ? this.getStellarTimesFrom(stellarData.tomorrow, moment().add(1, "days").format("YYYY-MM-DD")) : undefined;
|
||||
|
||||
weather.date = moment(forecast.time);
|
||||
weather.windSpeed = forecast.data.instant.details.wind_speed;
|
||||
weather.windFromDirection = forecast.data.instant.details.wind_from_direction;
|
||||
weather.temperature = forecast.data.instant.details.air_temperature;
|
||||
weather.minTemperature = forecast.minTemperature;
|
||||
weather.maxTemperature = forecast.maxTemperature;
|
||||
weather.weatherType = forecast.weatherType;
|
||||
weather.humidity = forecast.data.instant.details.relative_humidity;
|
||||
weather.precipitationAmount = forecast.precipitationAmount;
|
||||
weather.precipitationProbability = forecast.precipitationProbability;
|
||||
weather.precipitationUnits = units.precipitation_amount;
|
||||
|
||||
if (stellarTimesToday) {
|
||||
weather.sunset = moment(stellarTimesToday.sunset.time);
|
||||
weather.sunrise = weather.sunset < moment() && stellarTimesTomorrow ? moment(stellarTimesTomorrow.sunrise.time) : moment(stellarTimesToday.sunrise.time);
|
||||
}
|
||||
|
||||
return weather;
|
||||
},
|
||||
|
||||
convertWeatherType(weatherType, weatherTime) {
|
||||
const weatherHour = moment(weatherTime).format("HH");
|
||||
|
||||
const weatherTypes = {
|
||||
clearsky_day: "day-sunny",
|
||||
clearsky_night: "night-clear",
|
||||
clearsky_polartwilight: weatherHour < 14 ? "sunrise" : "sunset",
|
||||
cloudy: "cloudy",
|
||||
fair_day: "day-sunny-overcast",
|
||||
fair_night: "night-alt-partly-cloudy",
|
||||
fair_polartwilight: "day-sunny-overcast",
|
||||
fog: "fog",
|
||||
heavyrain: "rain", // Possibly raindrops or raindrop
|
||||
heavyrainandthunder: "thunderstorm",
|
||||
heavyrainshowers_day: "day-rain",
|
||||
heavyrainshowers_night: "night-alt-rain",
|
||||
heavyrainshowers_polartwilight: "day-rain",
|
||||
heavyrainshowersandthunder_day: "day-thunderstorm",
|
||||
heavyrainshowersandthunder_night: "night-alt-thunderstorm",
|
||||
heavyrainshowersandthunder_polartwilight: "day-thunderstorm",
|
||||
heavysleet: "sleet",
|
||||
heavysleetandthunder: "day-sleet-storm",
|
||||
heavysleetshowers_day: "day-sleet",
|
||||
heavysleetshowers_night: "night-alt-sleet",
|
||||
heavysleetshowers_polartwilight: "day-sleet",
|
||||
heavysleetshowersandthunder_day: "day-sleet-storm",
|
||||
heavysleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||
heavysleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||
heavysnow: "snow-wind",
|
||||
heavysnowandthunder: "day-snow-thunderstorm",
|
||||
heavysnowshowers_day: "day-snow-wind",
|
||||
heavysnowshowers_night: "night-alt-snow-wind",
|
||||
heavysnowshowers_polartwilight: "day-snow-wind",
|
||||
heavysnowshowersandthunder_day: "day-snow-thunderstorm",
|
||||
heavysnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||
heavysnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
||||
lightrain: "rain-mix",
|
||||
lightrainandthunder: "thunderstorm",
|
||||
lightrainshowers_day: "day-rain-mix",
|
||||
lightrainshowers_night: "night-alt-rain-mix",
|
||||
lightrainshowers_polartwilight: "day-rain-mix",
|
||||
lightrainshowersandthunder_day: "thunderstorm",
|
||||
lightrainshowersandthunder_night: "thunderstorm",
|
||||
lightrainshowersandthunder_polartwilight: "thunderstorm",
|
||||
lightsleet: "day-sleet",
|
||||
lightsleetandthunder: "day-sleet-storm",
|
||||
lightsleetshowers_day: "day-sleet",
|
||||
lightsleetshowers_night: "night-alt-sleet",
|
||||
lightsleetshowers_polartwilight: "day-sleet",
|
||||
lightsnow: "snowflake-cold",
|
||||
lightsnowandthunder: "day-snow-thunderstorm",
|
||||
lightsnowshowers_day: "day-snow-wind",
|
||||
lightsnowshowers_night: "night-alt-snow-wind",
|
||||
lightsnowshowers_polartwilight: "day-snow-wind",
|
||||
lightssleetshowersandthunder_day: "day-sleet-storm",
|
||||
lightssleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||
lightssleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||
lightssnowshowersandthunder_day: "day-snow-thunderstorm",
|
||||
lightssnowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||
lightssnowshowersandthunder_polartwilight: "day-snow-thunderstorm",
|
||||
partlycloudy_day: "day-cloudy",
|
||||
partlycloudy_night: "night-alt-cloudy",
|
||||
partlycloudy_polartwilight: "day-cloudy",
|
||||
rain: "rain",
|
||||
rainandthunder: "thunderstorm",
|
||||
rainshowers_day: "day-rain",
|
||||
rainshowers_night: "night-alt-rain",
|
||||
rainshowers_polartwilight: "day-rain",
|
||||
rainshowersandthunder_day: "thunderstorm",
|
||||
rainshowersandthunder_night: "lightning",
|
||||
rainshowersandthunder_polartwilight: "thunderstorm",
|
||||
sleet: "sleet",
|
||||
sleetandthunder: "day-sleet-storm",
|
||||
sleetshowers_day: "day-sleet",
|
||||
sleetshowers_night: "night-alt-sleet",
|
||||
sleetshowers_polartwilight: "day-sleet",
|
||||
sleetshowersandthunder_day: "day-sleet-storm",
|
||||
sleetshowersandthunder_night: "night-alt-sleet-storm",
|
||||
sleetshowersandthunder_polartwilight: "day-sleet-storm",
|
||||
snow: "snowflake-cold",
|
||||
snowandthunder: "lightning",
|
||||
snowshowers_day: "day-snow-wind",
|
||||
snowshowers_night: "night-alt-snow-wind",
|
||||
snowshowers_polartwilight: "day-snow-wind",
|
||||
snowshowersandthunder_day: "day-snow-thunderstorm",
|
||||
snowshowersandthunder_night: "night-alt-snow-thunderstorm",
|
||||
snowshowersandthunder_polartwilight: "day-snow-thunderstorm"
|
||||
};
|
||||
|
||||
return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null;
|
||||
},
|
||||
|
||||
getStellarTimesFrom(stellarData, date) {
|
||||
for (const time of stellarData.location.time) {
|
||||
if (time.date === date) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
getForecastForXHoursFrom(weather) {
|
||||
if (this.config.currentForecastHours === 1) {
|
||||
if (weather.next_1_hours) {
|
||||
return weather.next_1_hours;
|
||||
} else if (weather.next_6_hours) {
|
||||
return weather.next_6_hours;
|
||||
} else {
|
||||
return weather.next_12_hours;
|
||||
}
|
||||
} else if (this.config.currentForecastHours === 6) {
|
||||
if (weather.next_6_hours) {
|
||||
return weather.next_6_hours;
|
||||
} else if (weather.next_12_hours) {
|
||||
return weather.next_12_hours;
|
||||
} else {
|
||||
return weather.next_1_hours;
|
||||
}
|
||||
} else {
|
||||
if (weather.next_12_hours) {
|
||||
return weather.next_12_hours;
|
||||
} else if (weather.next_6_hours) {
|
||||
return weather.next_6_hours;
|
||||
} else {
|
||||
return weather.next_1_hours;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
fetchWeatherHourly() {
|
||||
this.getWeatherForecast("hourly")
|
||||
.then((forecast) => {
|
||||
this.setWeatherHourly(forecast);
|
||||
this.updateAvailable();
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
});
|
||||
},
|
||||
|
||||
async getWeatherForecast(type) {
|
||||
const getRequests = [this.getWeatherData(), this.getStellarData()];
|
||||
const [weatherData, stellarData] = await Promise.all(getRequests);
|
||||
if (!weatherData.properties.timeseries || !weatherData.properties.timeseries[0]) {
|
||||
Log.error("No weather data available.");
|
||||
return;
|
||||
}
|
||||
if (!stellarData) {
|
||||
Log.warn("No stellar data available.");
|
||||
}
|
||||
let forecasts;
|
||||
switch (type) {
|
||||
case "hourly":
|
||||
forecasts = this.getHourlyForecastFrom(weatherData);
|
||||
break;
|
||||
case "daily":
|
||||
default:
|
||||
forecasts = this.getDailyForecastFrom(weatherData);
|
||||
break;
|
||||
}
|
||||
const series = [];
|
||||
for (const forecast of forecasts) {
|
||||
series.push(this.getWeatherDataFrom(forecast, stellarData, weatherData.properties.meta.units));
|
||||
}
|
||||
return series;
|
||||
},
|
||||
|
||||
getHourlyForecastFrom(weatherData) {
|
||||
const series = [];
|
||||
|
||||
for (const forecast of weatherData.properties.timeseries) {
|
||||
forecast.symbol = forecast.data.next_1_hours?.summary?.symbol_code;
|
||||
forecast.precipitationAmount = forecast.data.next_1_hours?.details?.precipitation_amount;
|
||||
forecast.precipitationProbability = forecast.data.next_1_hours?.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = forecast.data.next_1_hours?.details?.air_temperature_min;
|
||||
forecast.maxTemperature = forecast.data.next_1_hours?.details?.air_temperature_max;
|
||||
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
||||
series.push(forecast);
|
||||
}
|
||||
return series;
|
||||
},
|
||||
|
||||
getDailyForecastFrom(weatherData) {
|
||||
const series = [];
|
||||
|
||||
const days = weatherData.properties.timeseries.reduce(function (days, forecast) {
|
||||
const date = moment(forecast.time).format("YYYY-MM-DD");
|
||||
days[date] = days[date] || [];
|
||||
days[date].push(forecast);
|
||||
return days;
|
||||
}, Object.create(null));
|
||||
|
||||
Object.keys(days).forEach(function (time, index) {
|
||||
let minTemperature = undefined;
|
||||
let maxTemperature = undefined;
|
||||
|
||||
//Default to first entry
|
||||
let forecast = days[time][0];
|
||||
forecast.symbol = forecast.data.next_12_hours?.summary?.symbol_code;
|
||||
forecast.precipitation = forecast.data.next_12_hours?.details?.precipitation_amount;
|
||||
|
||||
//Coming days
|
||||
let forecastDiffToEight = undefined;
|
||||
for (const timeseries of days[time]) {
|
||||
if (!timeseries.data.next_6_hours) continue; //next_6_hours has the most data
|
||||
|
||||
if (!minTemperature || timeseries.data.next_6_hours.details.air_temperature_min < minTemperature) minTemperature = timeseries.data.next_6_hours.details.air_temperature_min;
|
||||
if (!maxTemperature || maxTemperature < timeseries.data.next_6_hours.details.air_temperature_max) maxTemperature = timeseries.data.next_6_hours.details.air_temperature_max;
|
||||
|
||||
let closestTime = Math.abs(moment(timeseries.time).local().set({ hour: 8, minute: 0, second: 0, millisecond: 0 }).diff(moment(timeseries.time).local()));
|
||||
if ((forecastDiffToEight === undefined || closestTime < forecastDiffToEight) && timeseries.data.next_12_hours) {
|
||||
forecastDiffToEight = closestTime;
|
||||
forecast = timeseries;
|
||||
}
|
||||
}
|
||||
const forecastXHours = forecast.data.next_12_hours ?? forecast.data.next_6_hours ?? forecast.data.next_1_hours;
|
||||
if (forecastXHours) {
|
||||
forecast.symbol = forecastXHours.summary?.symbol_code;
|
||||
forecast.precipitationAmount = forecastXHours.details?.precipitation_amount ?? forecast.data.next_6_hours?.details?.precipitation_amount; // 6 hours is likely to have precipitation amount even if 12 hours does not
|
||||
forecast.precipitationProbability = forecastXHours.details?.probability_of_precipitation;
|
||||
forecast.minTemperature = minTemperature;
|
||||
forecast.maxTemperature = maxTemperature;
|
||||
|
||||
series.push(forecast);
|
||||
}
|
||||
});
|
||||
for (const forecast of series) {
|
||||
forecast.weatherType = this.convertWeatherType(forecast.symbol, forecast.time);
|
||||
}
|
||||
return series;
|
||||
},
|
||||
|
||||
fetchWeatherForecast() {
|
||||
this.getWeatherForecast("daily")
|
||||
.then((forecast) => {
|
||||
this.setWeatherForecast(forecast);
|
||||
this.updateAvailable();
|
||||
})
|
||||
.catch((error) => {
|
||||
Log.error(error);
|
||||
throw new Error(error);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user