first commit

This commit is contained in:
2023-09-30 20:18:17 +02:00
commit d5047f6ee0
293 changed files with 40054 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
# Weather Module
This module will be configurable to be used as a current weather view, or to show the forecast. This way the module can be used twice to fulfill both purposes.
For configuration options, please check the [MagicMirror² documentation](https://docs.magicmirror.builders/modules/weather.html).
+89
View File
@@ -0,0 +1,89 @@
{% if current %}
{% if not config.onlyTemp %}
<div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span>
<span>
{{ current.windSpeed | unit("wind") | round }}
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg);"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
&nbsp;
</sup>
{% endif %}
</span>
{% if config.showHumidity and current.humidity %}
<span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup>
{% endif %}
{% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
<span>
{% if current.nextSunAction() === "sunset" %}
{{ current.sunset | formatTime }}
{% else %}
{{ current.sunrise | formatTime }}
{% endif %}
</span>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
<div class="wi dimmed wi-hot"></div>
{{ current.uv_index }}
</td>
{% endif %}
</div>
{% endif %}
<div class="large light">
<span class="wi weathericon wi-{{current.weatherType}}"></span>
<span class="bright">
{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
<div class="normal light indoor">
{% if config.showIndoorTemperature and indoor.temperature %}
<div>
<span class="fas fa-home"></span>
<span class="bright">
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</div>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<div>
<span class="fas fa-tint"></span>
<span class="bright">
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
</div>
{% endif %}
</div>
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
<div class="normal medium feelslike">
{% if config.showFeelsLike %}
<span class="dimmed">
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span><br/>
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
</span><br/>
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
</span>
{% endif %}
</div>
{% endif %}
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `current` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
+52
View File
@@ -0,0 +1,52 @@
{% if forecast %}
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% if config.ignoreToday %}
{% set forecast = forecast.splice(1) %}
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format('ddd') }}</td>
{% endif %}
<td class="bright weather-icon"><span class="wi weathericon wi-{{ f.weatherType }}"></span></td>
<td class="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
<td class="align-right min-temp">
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">
{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
</td>
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">
{{ f.precipitationProbability | unit("precip", "%") }}
</td>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right dimmed uv-index">
{{ f.uv_index }}
<span class="wi dimmed weathericon wi-hot"></span>
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
+42
View File
@@ -0,0 +1,42 @@
{% if hourly %}
{% set numSteps = hourly | calcNumEntries %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% set hours = hourly.slice(0, numSteps) %}
{% for hour in hours %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ hour.date | formatTime }}</td>
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
<td class="align-right bright">
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
{% if hour.uv_index!=0 %}
{{ hour.uv_index }}
<span class="wi weathericon wi-hot"></span>
{% endif %}
</td>
{% endif %}
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
</td>
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">
{{ hour.precipitationProbability | unit("precip", "%") }}
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
@@ -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;
}
});
+334
View File
@@ -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;
}
});
+632
View File
@@ -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);
});
}
});
+49
View File
@@ -0,0 +1,49 @@
.weather .weathericon,
.weather .fa-home {
font-size: 75%;
line-height: 65px;
display: inline-block;
transform: translate(0, -3px);
}
.weather .humidity-icon {
padding-right: 4px;
}
.weather .humidity-padding {
padding-bottom: 6px;
}
.weather .day {
padding-left: 0;
padding-right: 25px;
}
.weather .weather-icon {
padding-right: 30px;
text-align: center;
}
.weather .min-temp {
padding-left: 20px;
padding-right: 0;
}
.weather .precipitation-amount,
.weather .precipitation-prob,
.weather .uv-index {
padding-left: 20px;
padding-right: 0;
}
.weather tr .weathericon {
line-height: 25px;
}
.weather tr.colored .min-temp {
color: #bcddff;
}
.weather tr.colored .max-temp {
color: #ff8e99;
}
+297
View File
@@ -0,0 +1,297 @@
/* global WeatherProvider, WeatherUtils, formatTime */
/* MagicMirror²
* Module: Weather
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*/
Module.register("weather", {
// Default module config.
defaults: {
weatherProvider: "openweathermap",
roundTemp: false,
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
lang: config.language,
units: config.units,
tempUnits: config.units,
windUnits: config.units,
timeFormat: config.timeFormat,
updateInterval: 10 * 60 * 1000, // every 10 minutes
animationSpeed: 1000,
showFeelsLike: true,
showHumidity: false,
showIndoorHumidity: false,
showIndoorTemperature: false,
showPeriod: true,
showPeriodUpper: false,
showPrecipitationAmount: false,
showPrecipitationProbability: false,
showUVIndex: false,
showSun: true,
showWindDirection: true,
showWindDirectionAsArrow: false,
degreeLabel: false,
decimalSymbol: ".",
maxNumberOfDays: 5,
maxEntries: 5,
ignoreToday: false,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
initialLoadDelay: 0, // 0 seconds delay
appendLocationNameToHeader: true,
calendarClass: "calendar",
tableClass: "small",
onlyTemp: false,
colored: false,
absoluteDates: false,
hourlyForecastIncrements: 1
},
// Module properties.
weatherProvider: null,
// Can be used by the provider to display location of event if nothing else is specified
firstEvent: null,
// Define required scripts.
getStyles: function () {
return ["font-awesome.css", "weather-icons.css", "weather.css"];
},
// Return the scripts that are necessary for the weather module.
getScripts: function () {
return ["moment.js", this.file("../utils.js"), "weatherutils.js", "weatherprovider.js", "weatherobject.js", "suncalc.js", this.file(`providers/${this.config.weatherProvider.toLowerCase()}.js`)];
},
// Override getHeader method.
getHeader: function () {
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
if (this.data.header) return `${this.data.header} ${this.weatherProvider.fetchedLocation()}`;
else return this.weatherProvider.fetchedLocation();
}
return this.data.header ? this.data.header : "";
},
// Start the weather module.
start: function () {
moment.locale(this.config.lang);
if (this.config.useKmh) {
Log.warn("Your are using the deprecated config values 'useKmh'. Please switch to windUnits!");
this.windUnits = "kmh";
} else if (this.config.useBeaufort) {
Log.warn("Your are using the deprecated config values 'useBeaufort'. Please switch to windUnits!");
this.windUnits = "beaufort";
}
// Initialize the weather provider.
this.weatherProvider = WeatherProvider.initialize(this.config.weatherProvider, this);
// Let the weather provider know we are starting.
this.weatherProvider.start();
// Add custom filters
this.addFilters();
// Schedule the first update.
this.scheduleUpdate(this.config.initialLoadDelay);
},
// Override notification handler.
notificationReceived: function (notification, payload, sender) {
if (notification === "CALENDAR_EVENTS") {
const senderClasses = sender.data.classes.toLowerCase().split(" ");
if (senderClasses.indexOf(this.config.calendarClass.toLowerCase()) !== -1) {
this.firstEvent = null;
for (let event of payload) {
if (event.location || event.geo) {
this.firstEvent = event;
Log.debug("First upcoming event with location: ", event);
break;
}
}
}
} else if (notification === "INDOOR_TEMPERATURE") {
this.indoorTemperature = this.roundValue(payload);
this.updateDom(300);
} else if (notification === "INDOOR_HUMIDITY") {
this.indoorHumidity = this.roundValue(payload);
this.updateDom(300);
}
},
// Select the template depending on the display type.
getTemplate: function () {
switch (this.config.type.toLowerCase()) {
case "current":
return "current.njk";
case "hourly":
return "hourly.njk";
case "daily":
case "forecast":
return "forecast.njk";
//Make the invalid values use the "Loading..." from forecast
default:
return "forecast.njk";
}
},
// Add all the data to the template.
getTemplateData: function () {
const currentData = this.weatherProvider.currentWeather();
const forecastData = this.weatherProvider.weatherForecast();
// Skip some hourly forecast entries if configured
const hourlyData = this.weatherProvider.weatherHourly()?.filter((e, i) => (i + 1) % this.config.hourlyForecastIncrements === this.config.hourlyForecastIncrements - 1);
return {
config: this.config,
current: currentData,
forecast: forecastData,
hourly: hourlyData,
indoor: {
humidity: this.indoorHumidity,
temperature: this.indoorTemperature
}
};
},
// What to do when the weather provider has new information available?
updateAvailable: function () {
Log.log("New weather information available.");
this.updateDom(0);
this.scheduleUpdate();
if (this.weatherProvider.currentWeather()) {
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
}
const notificationPayload = {
currentWeather: this.weatherProvider?.currentWeatherObject?.simpleClone() ?? null,
forecastArray: this.weatherProvider?.weatherForecastArray?.map((ar) => ar.simpleClone()) ?? [],
hourlyArray: this.weatherProvider?.weatherHourlyArray?.map((ar) => ar.simpleClone()) ?? [],
locationName: this.weatherProvider?.fetchedLocationName,
providerName: this.weatherProvider.providerName
};
this.sendNotification("WEATHER_UPDATED", notificationPayload);
},
scheduleUpdate: function (delay = null) {
let nextLoad = this.config.updateInterval;
if (delay !== null && delay >= 0) {
nextLoad = delay;
}
setTimeout(() => {
switch (this.config.type.toLowerCase()) {
case "current":
this.weatherProvider.fetchCurrentWeather();
break;
case "hourly":
this.weatherProvider.fetchWeatherHourly();
break;
case "daily":
case "forecast":
this.weatherProvider.fetchWeatherForecast();
break;
default:
Log.error(`Invalid type ${this.config.type} configured (must be one of 'current', 'hourly', 'daily' or 'forecast')`);
}
}, nextLoad);
},
roundValue: function (temperature) {
const decimals = this.config.roundTemp ? 0 : 1;
const roundValue = parseFloat(temperature).toFixed(decimals);
return roundValue === "-0" ? 0 : roundValue;
},
addFilters() {
this.nunjucksEnvironment().addFilter(
"formatTime",
function (date) {
return formatTime(this.config, date);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"unit",
function (value, type, valueUnit) {
let formattedValue;
if (type === "temperature") {
formattedValue = `${this.roundValue(WeatherUtils.convertTemp(value, this.config.tempUnits))}°`;
if (this.config.degreeLabel) {
if (this.config.tempUnits === "metric") {
formattedValue += "C";
} else if (this.config.tempUnits === "imperial") {
formattedValue += "F";
} else {
formattedValue += "K";
}
}
} else if (type === "precip") {
if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
formattedValue = "";
} else {
formattedValue = WeatherUtils.convertPrecipitationUnit(value, valueUnit, this.config.units);
}
} else if (type === "humidity") {
formattedValue = `${value}%`;
} else if (type === "wind") {
formattedValue = WeatherUtils.convertWind(value, this.config.windUnits);
}
return formattedValue;
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"roundValue",
function (value) {
return this.roundValue(value);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"decimalSymbol",
function (value) {
return value.toString().replace(/\./g, this.config.decimalSymbol);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"calcNumSteps",
function (forecast) {
return Math.min(forecast.length, this.config.maxNumberOfDays);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"calcNumEntries",
function (dataArray) {
return Math.min(dataArray.length, this.config.maxEntries);
}.bind(this)
);
this.nunjucksEnvironment().addFilter(
"opacity",
function (currentStep, numSteps) {
if (this.config.fade && this.config.fadePoint < 1) {
if (this.config.fadePoint < 0) {
this.config.fadePoint = 0;
}
const startingPoint = numSteps * this.config.fadePoint;
const numFadesteps = numSteps - startingPoint;
if (currentStep >= startingPoint) {
return 1 - (currentStep - startingPoint) / numFadesteps;
} else {
return 1;
}
} else {
return 1;
}
}.bind(this)
);
}
});
+136
View File
@@ -0,0 +1,136 @@
/* global SunCalc, WeatherUtils */
/* MagicMirror²
* Module: Weather
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
* This class is the blueprint for a day which includes weather information.
*
* Currently this is focused on the information which is necessary for the current weather.
* As soon as we start implementing the forecast, mode properties will be added.
*/
/**
* @external Moment
*/
class WeatherObject {
/**
* Constructor for a WeatherObject
*/
constructor() {
this.date = null;
this.windSpeed = null;
this.windFromDirection = null;
this.sunrise = null;
this.sunset = null;
this.temperature = null;
this.minTemperature = null;
this.maxTemperature = null;
this.weatherType = null;
this.humidity = null;
this.precipitationAmount = null;
this.precipitationUnits = null;
this.precipitationProbability = null;
this.feelsLikeTemp = null;
}
cardinalWindDirection() {
if (this.windFromDirection > 11.25 && this.windFromDirection <= 33.75) {
return "NNE";
} else if (this.windFromDirection > 33.75 && this.windFromDirection <= 56.25) {
return "NE";
} else if (this.windFromDirection > 56.25 && this.windFromDirection <= 78.75) {
return "ENE";
} else if (this.windFromDirection > 78.75 && this.windFromDirection <= 101.25) {
return "E";
} else if (this.windFromDirection > 101.25 && this.windFromDirection <= 123.75) {
return "ESE";
} else if (this.windFromDirection > 123.75 && this.windFromDirection <= 146.25) {
return "SE";
} else if (this.windFromDirection > 146.25 && this.windFromDirection <= 168.75) {
return "SSE";
} else if (this.windFromDirection > 168.75 && this.windFromDirection <= 191.25) {
return "S";
} else if (this.windFromDirection > 191.25 && this.windFromDirection <= 213.75) {
return "SSW";
} else if (this.windFromDirection > 213.75 && this.windFromDirection <= 236.25) {
return "SW";
} else if (this.windFromDirection > 236.25 && this.windFromDirection <= 258.75) {
return "WSW";
} else if (this.windFromDirection > 258.75 && this.windFromDirection <= 281.25) {
return "W";
} else if (this.windFromDirection > 281.25 && this.windFromDirection <= 303.75) {
return "WNW";
} else if (this.windFromDirection > 303.75 && this.windFromDirection <= 326.25) {
return "NW";
} else if (this.windFromDirection > 326.25 && this.windFromDirection <= 348.75) {
return "NNW";
} else {
return "N";
}
}
/**
* Determines if the sun sets or rises next. Uses the current time and not
* the date from the weather-forecast.
* @param {Moment} date an optional date where you want to get the next
* action for. Useful only in tests, defaults to the current time.
* @returns {string} "sunset" or "sunrise"
*/
nextSunAction(date = moment()) {
return date.isBetween(this.sunrise, this.sunset) ? "sunset" : "sunrise";
}
feelsLike() {
if (this.feelsLikeTemp) {
return this.feelsLikeTemp;
}
return WeatherUtils.calculateFeelsLike(this.temperature, this.windSpeed, this.humidity);
}
/**
* Checks if the weatherObject is at dayTime.
* @returns {boolean} true if it is at dayTime
*/
isDayTime() {
const now = !this.date ? moment() : this.date;
return now.isBetween(this.sunrise, this.sunset, undefined, "[]");
}
/**
* Update the sunrise / sunset time depending on the location. This can be
* used if your provider doesn't provide that data by itself. Then SunCalc
* is used here to calculate them according to the location.
* @param {number} lat latitude
* @param {number} lon longitude
*/
updateSunTime(lat, lon) {
const now = !this.date ? new Date() : this.date.toDate();
const times = SunCalc.getTimes(now, lat, lon);
this.sunrise = moment(times.sunrise);
this.sunset = moment(times.sunset);
}
/**
* Clone to simple object to prevent mutating and deprecation of legacy library.
*
* Before being handed to other modules, mutable values must be cloned safely.
* Especially 'moment' object is not immutable, so original 'date', 'sunrise', 'sunset' could be corrupted or changed by other modules.
* @returns {object} plained object clone of original weatherObject
*/
simpleClone() {
const toFlat = ["date", "sunrise", "sunset"];
let clone = { ...this };
for (const prop of toFlat) {
clone[prop] = clone?.[prop]?.valueOf() ?? clone?.[prop];
}
return clone;
}
}
/*************** DO NOT EDIT THE LINE BELOW ***************/
if (typeof module !== "undefined") {
module.exports = WeatherObject;
}
+168
View File
@@ -0,0 +1,168 @@
/* global Class, performWebRequest */
/* MagicMirror²
* Module: Weather
*
* By Michael Teeuw https://michaelteeuw.nl
* MIT Licensed.
*
* This class is the blueprint for a weather provider.
*/
const WeatherProvider = Class.extend({
// Weather Provider Properties
providerName: null,
defaults: {},
// The following properties have accessor methods.
// Try to not access them directly.
currentWeatherObject: null,
weatherForecastArray: null,
weatherHourlyArray: null,
fetchedLocationName: null,
// The following properties will be set automatically.
// You do not need to overwrite these properties.
config: null,
delegate: null,
providerIdentifier: null,
// Weather Provider Methods
// All the following methods can be overwritten, although most are good as they are.
// Called when a weather provider is initialized.
init: function (config) {
this.config = config;
Log.info(`Weather provider: ${this.providerName} initialized.`);
},
// Called to set the config, this config is the same as the weather module's config.
setConfig: function (config) {
this.config = config;
Log.info(`Weather provider: ${this.providerName} config set.`, this.config);
},
// Called when the weather provider is about to start.
start: function () {
Log.info(`Weather provider: ${this.providerName} started.`);
},
// This method should start the API request to fetch the current weather.
// This method should definitely be overwritten in the provider.
fetchCurrentWeather: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchCurrentWeather method.`);
},
// This method should start the API request to fetch the weather forecast.
// This method should definitely be overwritten in the provider.
fetchWeatherForecast: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
},
// This method should start the API request to fetch the weather hourly.
// This method should definitely be overwritten in the provider.
fetchWeatherHourly: function () {
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherHourly method.`);
},
// This returns a WeatherDay object for the current weather.
currentWeather: function () {
return this.currentWeatherObject;
},
// This returns an array of WeatherDay objects for the weather forecast.
weatherForecast: function () {
return this.weatherForecastArray;
},
// This returns an object containing WeatherDay object(s) depending on the type of call.
weatherHourly: function () {
return this.weatherHourlyArray;
},
// This returns the name of the fetched location or an empty string.
fetchedLocation: function () {
return this.fetchedLocationName || "";
},
// Set the currentWeather and notify the delegate that new information is available.
setCurrentWeather: function (currentWeatherObject) {
// We should check here if we are passing a WeatherDay
this.currentWeatherObject = currentWeatherObject;
},
// Set the weatherForecastArray and notify the delegate that new information is available.
setWeatherForecast: function (weatherForecastArray) {
// We should check here if we are passing a WeatherDay
this.weatherForecastArray = weatherForecastArray;
},
// Set the weatherHourlyArray and notify the delegate that new information is available.
setWeatherHourly: function (weatherHourlyArray) {
this.weatherHourlyArray = weatherHourlyArray;
},
// Set the fetched location name.
setFetchedLocation: function (name) {
this.fetchedLocationName = name;
},
// Notify the delegate that new weather is available.
updateAvailable: function () {
this.delegate.updateAvailable(this);
},
/**
* A convenience function to make requests.
* @param {string} url the url to fetch from
* @param {string} type what contenttype to expect in the response, can be "json" or "xml"
* @param {Array.<{name: string, value:string}>} requestHeaders the HTTP headers to send
* @param {Array.<string>} expectedResponseHeaders the expected HTTP headers to recieve
* @returns {Promise} resolved when the fetch is done
*/
fetchData: async function (url, type = "json", requestHeaders = undefined, expectedResponseHeaders = undefined) {
const mockData = this.config.mockData;
if (mockData) {
const data = mockData.substring(1, mockData.length - 1);
return JSON.parse(data);
}
const useCorsProxy = typeof this.config.useCorsProxy !== "undefined" && this.config.useCorsProxy;
return performWebRequest(url, type, useCorsProxy, requestHeaders, expectedResponseHeaders);
}
});
/**
* Collection of registered weather providers.
*/
WeatherProvider.providers = [];
/**
* Static method to register a new weather provider.
* @param {string} providerIdentifier The name of the weather provider
* @param {object} providerDetails The details of the weather provider
*/
WeatherProvider.register = function (providerIdentifier, providerDetails) {
WeatherProvider.providers[providerIdentifier.toLowerCase()] = WeatherProvider.extend(providerDetails);
};
/**
* Static method to initialize a new weather provider.
* @param {string} providerIdentifier The name of the weather provider
* @param {object} delegate The weather module
* @returns {object} The new weather provider
*/
WeatherProvider.initialize = function (providerIdentifier, delegate) {
const pi = providerIdentifier.toLowerCase();
const provider = new WeatherProvider.providers[pi]();
const config = Object.assign({}, provider.defaults, delegate.config);
provider.delegate = delegate;
provider.setConfig(config);
provider.providerIdentifier = pi;
if (!provider.providerName) {
provider.providerName = pi;
}
return provider;
};
+142
View File
@@ -0,0 +1,142 @@
/* MagicMirror²
* Weather Util Methods
*
* By Rejas
* MIT Licensed.
*/
const WeatherUtils = {
/**
* Convert wind (from m/s) to beaufort scale
* @param {number} speedInMS the windspeed you want to convert
* @returns {number} the speed in beaufort
*/
beaufortWindSpeed(speedInMS) {
const windInKmh = this.convertWind(speedInMS, "kmh");
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
for (const [index, speed] of speeds.entries()) {
if (speed > windInKmh) {
return index;
}
}
return 12;
},
/**
* Convert a value in a given unit to a string with a converted
* value and a postfix matching the output unit system.
* @param {number} value - The value to convert.
* @param {string} valueUnit - The unit the values has. Default is mm.
* @param {string} outputUnit - The unit system (imperial/metric) the return value should have.
* @returns {string} - A string with tha value and a unit postfix.
*/
convertPrecipitationUnit(value, valueUnit, outputUnit) {
if (valueUnit === "%") return `${value.toFixed(0)} ${valueUnit}`;
let convertedValue = value;
let conversionUnit = valueUnit;
if (outputUnit === "imperial") {
if (valueUnit && valueUnit.toLowerCase() === "cm") convertedValue = convertedValue * 0.3937007874;
else convertedValue = convertedValue * 0.03937007874;
conversionUnit = "in";
} else {
conversionUnit = valueUnit ? valueUnit : "mm";
}
return `${convertedValue.toFixed(2)} ${conversionUnit}`;
},
/**
* Convert temp (from degrees C) into imperial or metric unit depending on
* your config
* @param {number} tempInC the temperature in celsius you want to convert
* @param {string} unit can be 'imperial' or 'metric'
* @returns {number} the converted temperature
*/
convertTemp(tempInC, unit) {
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
},
/**
* Convert wind speed into another unit.
* @param {number} windInMS the windspeed in meter/sec you want to convert
* @param {string} unit can be 'beaufort', 'kmh', 'knots, 'imperial' (mph)
* or 'metric' (mps)
* @returns {number} the converted windspeed
*/
convertWind(windInMS, unit) {
switch (unit) {
case "beaufort":
return this.beaufortWindSpeed(windInMS);
case "kmh":
return (windInMS * 3600) / 1000;
case "knots":
return windInMS * 1.943844;
case "imperial":
return windInMS * 2.2369362920544;
case "metric":
default:
return windInMS;
}
},
/*
* Convert the wind direction cardinal to value
*/
convertWindDirection(windDirection) {
const windCardinals = {
N: 0,
NNE: 22,
NE: 45,
ENE: 67,
E: 90,
ESE: 112,
SE: 135,
SSE: 157,
S: 180,
SSW: 202,
SW: 225,
WSW: 247,
W: 270,
WNW: 292,
NW: 315,
NNW: 337
};
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
},
convertWindToMetric(mph) {
return mph / 2.2369362920544;
},
convertWindToMs(kmh) {
return kmh * 0.27777777777778;
},
calculateFeelsLike(temperature, windSpeed, humidity) {
const windInMph = this.convertWind(windSpeed, "imperial");
const tempInF = this.convertTemp(temperature, "imperial");
let feelsLike = tempInF;
if (windInMph > 3 && tempInF < 50) {
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
} else if (tempInF > 80 && humidity > 40) {
feelsLike =
-42.379 +
2.04901523 * tempInF +
10.14333127 * humidity -
0.22475541 * tempInF * humidity -
6.83783 * Math.pow(10, -3) * tempInF * tempInF -
5.481717 * Math.pow(10, -2) * humidity * humidity +
1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity +
8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity -
1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
}
return ((feelsLike - 32) * 5) / 9;
}
};
if (typeof module !== "undefined") {
module.exports = WeatherUtils;
}