123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- /* global WeatherProvider, WeatherObject */
-
- /* Magic Mirror
- * 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)],
- * windUnits: "mps" | "kph" | "mph" (default)
- * tempUnits: "imperial" | "metric" (default)
- *
- * 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,
- windUnits: "mph"
- },
-
- // 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 useable
- 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're new data available
- .finally(() => this.updateAvailable());
- },
-
- // Create a WeatherObject using current weather data (data for the current hour)
- generateWeatherObjectFromCurrentWeather(currentWeatherData) {
- const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
-
- // 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 = this.convertWindSpeed(forecastDataHours[hour].windSpeed10m);
- currentWeather.windDirection = forecastDataHours[hour].windDirectionFrom10m;
- currentWeather.temperature = this.convertTemp(forecastDataHours[hour].screenTemperature);
- currentWeather.minTemperature = this.convertTemp(forecastDataHours[hour].minScreenAirTemp);
- currentWeather.maxTemperature = this.convertTemp(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.precipitation = forecastDataHours[hour].probOfPrecipitation;
- currentWeather.feelsLikeTemp = this.convertTemp(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 doesnt 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 useable
- 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're 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(this.config.units, this.config.tempUnits, this.config.windUnits, this.config.useKmh);
-
- // 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 = this.convertTemp(forecastDataDays[day].nightMinScreenTemperature);
- forecastWeather.maxTemperature = this.convertTemp(forecastDataDays[day].dayMaxScreenTemperature);
-
- // Using daytime forecast values
- forecastWeather.windSpeed = this.convertWindSpeed(forecastDataDays[day].midday10MWindSpeed);
- forecastWeather.windDirection = forecastDataDays[day].midday10MWindDirection;
- forecastWeather.weatherType = this.convertWeatherType(forecastDataDays[day].daySignificantWeatherCode);
- forecastWeather.precipitation = 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 = this.convertTemp(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;
- },
-
- // Convert temperatures to Fahrenheit (from degrees C), if required
- convertTemp(tempInC) {
- return this.config.tempUnits === "imperial" ? (tempInC * 9) / 5 + 32 : tempInC;
- },
-
- // Convert wind speed from metres per second
- // To keep the supplied metres per second units, use "mps"
- // To use kilometres per hour, use "kph"
- // Else assumed imperial and the value is returned in miles per hour (a Met Office user is likely to be UK-based)
- convertWindSpeed(windInMpS) {
- if (this.config.windUnits === "mps") {
- return windInMpS;
- }
-
- if (this.config.windUnits === "kph" || this.config.windUnits === "metric" || this.config.useKmh) {
- return windInMpS * 3.6;
- }
-
- return windInMpS * 2.23694;
- },
-
- // 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;
- }
- });
|