mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 11:50:00 +00:00
In the latest versions of ESLint, more and more formatting rules were removed or declared deprecated. These rules have been integrated into the new Stylistic package (https://eslint.style/guide/why) and expanded. Stylistic acts as a better formatter for JavaScript as Prettier. With this PR there are many changes that make the code more uniform, but it may be difficult to review due to the large amount. Even if I have no worries about the changes, perhaps this would be something for the release after next. Let me know what you think.
335 lines
12 KiB
JavaScript
335 lines
12 KiB
JavaScript
/* 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 "";
|
|
}
|
|
}
|
|
});
|