Skip to content

Commit

Permalink
Merge pull request #2058 from XBCreepinJesus/develop
Browse files Browse the repository at this point in the history
UK Met Office DataHub weather provider
  • Loading branch information
MichMich authored Jun 28, 2020
2 parents c4bc3e2 + b91fccc commit fdd389d
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ _This release is scheduled to be released on 2020-07-01._
- Added option to config the level of logging
- Added prettier for an even cleaner codebase
- Hide Sunrise/Sunset in Weather module
- Added Met Office DataHub (UK) provider

### Updated

Expand Down
303 changes: 303 additions & 0 deletions modules/default/weather/providers/ukmetofficedatahub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
/* 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)",

// 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;
if (this.config.appendLocationNameToHeader) {
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() {
let headers = {
accept: "application/json",
"x-ibm-client-id": this.config.apiKey,
"x-ibm-client-secret": this.config.apiSecret
};

return headers;
},

// 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.info(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);

// 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 (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, height} to calcAstroData
// Could just pass lat/long from this.config, but returned data from MO also contains elevation
let times = this.calcAstroData(currentWeatherData.features[0].geometry.coordinates);
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];

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.info(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 (day in forecastDataDays) {
const forecastWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);

// 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;
},

// Calculate sunrise/sunset times
calcAstroData(location) {
const sunTimes = [];

// Careful to pass values to SunCalc in correct order (latitude, longitude, elevation)
let times = SunCalc.getTimes(new Date(), location[1], location[0], location[2]);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));

return sunTimes;
},

// 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") {
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;
}
});
2 changes: 1 addition & 1 deletion modules/default/weather/weather.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ Module.register("weather", {
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
value = "";
} else {
if (this.config.weatherProvider === "ukmetoffice") {
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
value += "%";
} else {
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
Expand Down

0 comments on commit fdd389d

Please sign in to comment.