2020-12-19 11:13:46 +01:00
/* global WeatherProvider, WeatherObject, SunCalc */
/ * M a g i c M i r r o r
* 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" ,
2021-01-23 11:21:56 +01:00
// Set the default config properties that is specific to this provider
defaults : {
lat : 0 ,
lon : 0 ,
precipitationValue : "pmedian"
} ,
2020-12-19 11:13:46 +01:00
/ * *
* 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
2020-12-21 11:23:02 +01:00
*
* @ param config
2020-12-19 11:13:46 +01:00
* /
setConfig ( config ) {
this . config = config ;
2021-06-30 15:53:51 +02:00
if ( ! config . precipitationValue || [ "pmin" , "pmean" , "pmedian" , "pmax" ] . indexOf ( config . precipitationValue ) === - 1 ) {
2020-12-19 11:13:46 +01:00
console . log ( "invalid or not set: " + config . precipitationValue ) ;
2021-01-23 11:21:56 +01:00
config . precipitationValue = this . defaults . precipitationValue ;
2020-12-19 11:13:46 +01:00
}
} ,
/ * *
* 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 .
2020-12-21 11:23:02 +01:00
*
* @ param times
2020-12-19 11:13:46 +01:00
* /
getClosestToCurrentTime ( times ) {
let now = moment ( ) ;
let minDiff = undefined ;
2021-04-18 14:53:15 +02:00
for ( const time of times ) {
2020-12-19 11:13:46 +01:00
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 .
2020-12-21 11:23:02 +01:00
*
* @ param weatherData
* @ param coordinates
* @ param weatherData
* @ param coordinates
2020-12-19 11:13:46 +01:00
* /
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 .
2020-12-21 11:23:02 +01:00
*
* @ param allWeatherData
* @ param coordinates
* @ param allWeatherData
* @ param coordinates
2020-12-19 11:13:46 +01:00
* /
convertWeatherDataGroupedByDay ( allWeatherData , coordinates ) {
2021-04-18 14:53:15 +02:00
let currentWeather ;
2020-12-19 11:13:46 +01:00
let result = [ ] ;
let allWeatherObjects = this . fillInGaps ( allWeatherData ) . map ( ( weatherData ) => this . convertWeatherDataToObject ( weatherData , coordinates ) ) ;
2021-04-18 14:53:15 +02:00
let dayWeatherTypes = [ ] ;
2020-12-19 11:13:46 +01:00
2021-04-18 14:53:15 +02:00
for ( const weatherObject of allWeatherObjects ) {
2020-12-19 11:13:46 +01:00
//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 )
2020-12-21 11:23:02 +01:00
*
* @ param data
2020-12-19 11:13:46 +01:00
* /
resolveCoordinates ( data ) {
return { lat : data . geometry . coordinates [ 0 ] [ 1 ] , lon : data . geometry . coordinates [ 0 ] [ 0 ] } ;
} ,
/ * *
2020-12-21 11:23:02 +01:00
* Checks if the weatherObject is at dayTime .
*
* @ param weatherObject
2020-12-19 11:13:46 +01:00
* /
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 .
2020-12-21 11:23:02 +01:00
*
* @ param data
2020-12-19 11:13:46 +01:00
* /
fillInGaps ( data ) {
let result = [ ] ;
2021-06-30 16:00:26 +02:00
for ( let i = 1 ; i < data . length ; i ++ ) {
2020-12-19 11:13:46 +01:00
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
2021-06-30 16:00:26 +02:00
for ( let j = 0 ; j < hours ; j ++ ) {
2020-12-19 11:13:46 +01:00
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 .
2020-12-21 11:23:02 +01:00
*
* @ param currentWeatherData
* @ param name
* @ param currentWeatherData
* @ param name
2020-12-19 11:13:46 +01:00
* /
paramValue ( currentWeatherData , name ) {
2021-06-30 15:53:51 +02:00
return currentWeatherData . parameters . filter ( ( p ) => p . name === name ) . flatMap ( ( p ) => p . values ) [ 0 ] ;
2020-12-19 11:13:46 +01:00
} ,
/ * *
* 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 .
2020-12-21 11:23:02 +01:00
*
* @ param input
* @ param isDayTime
* @ param input
* @ param isDayTime
2020-12-19 11:13:46 +01:00
* /
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 "" ;
}
}
} ) ;