Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UK Met Office DataHub weather provider #2058

Merged
merged 4 commits into from
Jun 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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