mirror of
				https://github.com/MichMich/MagicMirror.git
				synced 2025-10-31 18:54:51 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* global WeatherProvider, WeatherObject */
 | |
| 
 | |
| /*
 | |
|  * This class is a provider for SMHI (Sweden only).
 | |
|  * Metric system is the only supported unit,
 | |
|  * see https://www.smhi.se/
 | |
|  */
 | |
| 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 {string} 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 "";
 | |
| 		}
 | |
| 	}
 | |
| });
 |