mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
Adding SMHI as a provider for the weather module
This commit is contained in:
parent
3024319027
commit
3f851c1fd6
@ -15,6 +15,7 @@ _This release is scheduled to be released on 2021-01-01._
|
|||||||
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
- Added new parameter "useKmh" to weather module for displaying wind speed as kmh.
|
||||||
- Chuvash translation.
|
- Chuvash translation.
|
||||||
- Added Weatherbit as a provider to Weather module.
|
- Added Weatherbit as a provider to Weather module.
|
||||||
|
- Added SMHI as a provider to Weather module.
|
||||||
- Added Hindi & Gujarati translation.
|
- Added Hindi & Gujarati translation.
|
||||||
- Chuvash translation.
|
- Chuvash translation.
|
||||||
- Calendar: new options "limitDays" and "coloredEvents"
|
- Calendar: new options "limitDays" and "coloredEvents"
|
||||||
|
279
modules/default/weather/providers/smhi.js
Normal file
279
modules/default/weather/providers/smhi.js
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
/* global WeatherProvider, WeatherObject, SunCalc */
|
||||||
|
|
||||||
|
/* Magic Mirror
|
||||||
|
* Module: Weather
|
||||||
|
* Provider: SMHI
|
||||||
|
*
|
||||||
|
* By BuXXi https://github.com/buxxi
|
||||||
|
* MIT Licensed
|
||||||
|
*
|
||||||
|
* This class is a provider for SMHI (Sweden only).
|
||||||
|
* Note that SMHI doesn't provide sunrise and sundown, use SunCalc to calculate it.
|
||||||
|
* Metric system is the only supported unit.
|
||||||
|
*/
|
||||||
|
WeatherProvider.register("smhi", {
|
||||||
|
providerName: "SMHI",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements method in interface for fetching current weather
|
||||||
|
*/
|
||||||
|
fetchCurrentWeather() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let closest = this.getClosestToCurrentTime(data.timeSeries);
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObject = this.convertWeatherDataToObject(closest, coordinates);
|
||||||
|
this.setFetchedLocation(`(${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 forecast.
|
||||||
|
* Handling hourly forecast would be easy as not grouping by day but it seems really specific for one weather provider for now.
|
||||||
|
*/
|
||||||
|
fetchWeatherForecast() {
|
||||||
|
this.fetchData(this.getURL())
|
||||||
|
.then((data) => {
|
||||||
|
let coordinates = this.resolveCoordinates(data);
|
||||||
|
let weatherObjects = this.convertWeatherDataGroupedByDay(data.timeSeries, coordinates);
|
||||||
|
this.setFetchedLocation(`(${coordinates.lat},${coordinates.lon})`);
|
||||||
|
this.setWeatherForecast(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
|
||||||
|
*/
|
||||||
|
setConfig(config) {
|
||||||
|
this.config = config;
|
||||||
|
if (!config.precipitationValue || ["pmin", "pmean", "pmedian", "pmax"].indexOf(config.precipitationValue) == -1) {
|
||||||
|
console.log("invalid or not set: " + config.precipitationValue);
|
||||||
|
config.precipitationValue = "pmedian";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
getClosestToCurrentTime(times) {
|
||||||
|
let now = moment();
|
||||||
|
let minDiff = undefined;
|
||||||
|
for (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
|
||||||
|
*/
|
||||||
|
getURL() {
|
||||||
|
let lon = this.config.lon;
|
||||||
|
let lat = this.config.lat;
|
||||||
|
return `https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/geotype/point/lon/${lon}/lat/${lat}/data.json`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
convertWeatherDataToObject(weatherData, coordinates) {
|
||||||
|
let currentWeather = new WeatherObject("metric", "metric", "metric"); //Weather data is only for Sweden and nobody in Sweden would use imperial
|
||||||
|
|
||||||
|
currentWeather.date = moment(weatherData.validTime);
|
||||||
|
let times = SunCalc.getTimes(currentWeather.date.toDate(), coordinates.lat, coordinates.lon);
|
||||||
|
currentWeather.sunrise = moment(times.sunrise, "X");
|
||||||
|
currentWeather.sunset = moment(times.sunset, "X");
|
||||||
|
currentWeather.humidity = this.paramValue(weatherData, "r");
|
||||||
|
currentWeather.temperature = this.paramValue(weatherData, "t");
|
||||||
|
currentWeather.windSpeed = this.paramValue(weatherData, "ws");
|
||||||
|
currentWeather.windDirection = this.paramValue(weatherData, "wd");
|
||||||
|
currentWeather.weatherType = this.convertWeatherType(this.paramValue(weatherData, "Wsymb2"), this.isDayTime(currentWeather));
|
||||||
|
|
||||||
|
//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.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 2: // Snow and rain, treat it as 50/50 snow and rain
|
||||||
|
currentWeather.snow += precipitationValue / 2;
|
||||||
|
currentWeather.rain += precipitationValue / 2;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
case 3: // Rain
|
||||||
|
case 4: // Drizzle
|
||||||
|
case 5: // Freezing rain
|
||||||
|
case 6: // Freezing drizzle
|
||||||
|
currentWeather.rain += precipitationValue;
|
||||||
|
currentWeather.precipitation += precipitationValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentWeather;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes all of the data points and converts it to one WeatherObject per day.
|
||||||
|
*/
|
||||||
|
convertWeatherDataGroupedByDay(allWeatherData, coordinates) {
|
||||||
|
var currentWeather;
|
||||||
|
let result = [];
|
||||||
|
|
||||||
|
let allWeatherObjects = this.fillInGaps(allWeatherData).map((weatherData) => this.convertWeatherDataToObject(weatherData, coordinates));
|
||||||
|
var dayWeatherTypes = [];
|
||||||
|
|
||||||
|
for (weatherObject of allWeatherObjects) {
|
||||||
|
//If its the first object or if a day change we need to reset the summary object
|
||||||
|
if (!currentWeather || !currentWeather.date.isSame(weatherObject.date, "day")) {
|
||||||
|
currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
|
||||||
|
dayWeatherTypes = [];
|
||||||
|
currentWeather.date = weatherObject.date;
|
||||||
|
currentWeather.minTemperature = Infinity;
|
||||||
|
currentWeather.maxTemperature = -Infinity;
|
||||||
|
currentWeather.snow = 0;
|
||||||
|
currentWeather.rain = 0;
|
||||||
|
currentWeather.precipitation = 0;
|
||||||
|
result.push(currentWeather);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Keep track of what icons has been used for each hour of daytime and use the middle one for the forecast
|
||||||
|
if (this.isDayTime(weatherObject)) {
|
||||||
|
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.precipitation += weatherObject.precipitation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve coordinates from the response data (probably preferably to use this if it's not matching the config values exactly)
|
||||||
|
*/
|
||||||
|
resolveCoordinates(data) {
|
||||||
|
return { lat: data.geometry.coordinates[0][1], lon: data.geometry.coordinates[0][0] };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the weatherObject is at dayTime
|
||||||
|
*/
|
||||||
|
isDayTime(weatherObject) {
|
||||||
|
return weatherObject.date.isBetween(weatherObject.sunrise, weatherObject.sunset, undefined, "[]");
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
fillInGaps(data) {
|
||||||
|
let result = [];
|
||||||
|
for (var 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 (var 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 fetch a property from the returned data set.
|
||||||
|
* The returned values is an array with always one value in it.
|
||||||
|
*/
|
||||||
|
paramValue(currentWeatherData, name) {
|
||||||
|
return currentWeatherData.parameters.filter((p) => p.name == name).flatMap((p) => p.values)[0];
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the icon value from SHMI to an icon that MagicMirror understands.
|
||||||
|
* Uses different icons depending if its daytime or nighttime.
|
||||||
|
* SHMI's description of what the numeric value means is the comment after the case.
|
||||||
|
*/
|
||||||
|
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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user