Add new weather data provider UK Met Office (Datapoint)

This commit is contained in:
Malcolm Oakes 2019-04-03 15:19:32 +01:00
parent b508a629e8
commit c80e04fe8d
12 changed files with 2664 additions and 1190 deletions

12
modules/default/weather/README.md Normal file → Executable file
View File

@ -35,9 +35,9 @@ The following properties can be configured:
| Option | Description
| ---------------------------- | -----------
| `weatherProvider` | Which weather provider should be used. <br><br> **Possible values:** `openweathermap` , `darksky` , or `weathergov` <br> **Default value:** `openweathermap`
| `weatherProvider` | Which weather provider should be used. <br><br> **Possible values:** `openweathermap` , `darksky` , `weathergov` or `ukmetoffice`<br> **Default value:** `openweathermap`
| `type` | Which type of weather data should be displayed. <br><br> **Possible values:** `current` or `forecast` <br> **Default value:** `current`
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit <br> **Default value:** `config.units`
| `units` | What units to use. Specified by config.js <br><br> **Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit, `ukunits` = Celsius (wind speed mph) <br> **Default value:** `config.units`
| `roundTemp` | Round temperature value to nearest integer. <br><br> **Possible values:** `true` (round to integer) or `false` (display exact value with decimal point) <br> **Default value:** `false`
| `degreeLabel` | Show the degree label for your chosen units (Metric = C, Imperial = F, Kelvin = K). <br><br> **Possible values:** `true` or `false` <br> **Default value:** `false`
| `updateInterval` | How often does the content needs to be fetched? (Milliseconds) <br><br> **Possible values:** `1000` - `86400000` <br> **Default value:** `600000` (10 minutes)
@ -105,6 +105,14 @@ The following properties can be configured:
| `lat` | The geo coordinate latitude. <br><br> This value is **REQUIRED**
| `lon` | The geo coordinate longitude. <br><br> This value is **REQUIRED**
### UK Met Office options
| Option | Description
| ---------------------------- | -----------
| `apiBase` | The UKMO base URL. <br><br> **Possible value:** `'http://datapoint.metoffice.gov.uk/public/data/val/wxfcs/all/json/'` <br> This value is **REQUIRED**
| `locationId` | The UKMO API location code. <br><br> **Possible values:** `322942` <br> This value is **REQUIRED**
| `apiKey` | The [UK Met Office](https://www.metoffice.gov.uk/datapoint/getting-started) API key, which can be obtained by creating an UKMO Datapoint account. <br><br> This value is **REQUIRED**
## API Provider Development
If you want to add another API provider checkout the [Guide](providers).

17
modules/default/weather/current.njk Normal file → Executable file
View File

@ -9,7 +9,7 @@
{{ current.windSpeed | round }}
{% endif %}
{% if config.showWindDirection %}
<sup>
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fa fa-long-arrow-up" style="transform:rotate({{ current.windDirection }}deg);"></i>
{% else %}
@ -56,11 +56,18 @@
</div>
{% endif %}
</div>
{% if config.showFeelsLike and not config.onlyTemp %}
{% if (config.showFeelsLike or config.showPrecipitationAmount) and not config.onlyTemp %}
<div class="normal medium">
<span class="dimmed">
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
</span>
{% if config.showFeelsLike %}
<span class="dimmed">
{{ "FEELS" | translate }} {{ current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }}
</span>
{% endif %}
{% if config.showPrecipitationAmount %}
<span class="dimmed">
{{ "PRECIP" | translate }} {{ current.precipitation | unit("precip") }}
</span>
{% endif %}
</div>
{% endif %}
{% else %}

6
modules/default/weather/providers/README.md Normal file → Executable file
View File

@ -91,9 +91,9 @@ A convenience function to make requests. It returns a promise.
| Property | Type | Value/Unit |
| --- | --- | --- |
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric` and `imperial` |
| units | `string` | Gets initialized with the constructor. <br> Possible values: `metric`, `imperial` and `ukunits` |
| date | `object` | [Moment.js](https://momentjs.com/) object of the time/date. |
| windSpeed |`number` | Metric: `meter/second` <br> Imperial: `miles/hour` |
| windSpeed |`number` | Metric: `meter/second` <br> Imperial: `miles/hour` <br> UKunits: `miles/hour` |
| windDirection |`number` | Direction of the wind in degrees. |
| sunrise |`object` | [Moment.js](https://momentjs.com/) object of sunrise. |
| sunset |`object` | [Moment.js](https://momentjs.com/) object of sunset. |
@ -104,7 +104,7 @@ A convenience function to make requests. It returns a promise.
| humidity | `number` | Percentage of humidity |
| rain | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| snow | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` |
| precipitation | `number` | Metric: `millimeters` <br> Imperial: `inches` <br> Ukunits: `percent` |
#### Current weather

View File

@ -0,0 +1,263 @@
/* global WeatherProvider, WeatherObject */
/* Magic Mirror
* Module: Weather
*
* By Malcolm Oakes https://github.com/maloakes
* MIT Licensed.
*
* This class is a provider for UK Met Office Datapoint.
*/
WeatherProvider.register("ukmetoffice", {
// Set the name of the provider.
// This isn't strictly necessary, since it will fallback to the provider identifier
// But for debugging (and future alerts) it would be nice to have the real name.
providerName: "UK Met Office",
units: {
imperial: "us",
metric: "si",
ukunits: "uk"
},
// Overwrite the fetchCurrentWeather method.
fetchCurrentWeather() {
this.fetchData(this.getUrl("3hourly"))
.then(data => {
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location ||
!data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length == 0) {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
}
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
},
// Overwrite the fetchCurrentWeather method.
fetchWeatherForecast() {
this.fetchData(this.getUrl("daily"))
.then(data => {
if (!data || !data.SiteRep || !data.SiteRep.DV || !data.SiteRep.DV.Location ||
!data.SiteRep.DV.Location.Period || data.SiteRep.DV.Location.Period.length == 0) {
// Did not receive usable new data.
// Maybe this needs a better check?
return;
}
this.setFetchedLocation(`${data.SiteRep.DV.Location.name}, ${data.SiteRep.DV.Location.country}`);
const forecast = this.generateWeatherObjectsFromForecast(data);
this.setWeatherForecast(forecast);
})
.catch(function(request) {
Log.error("Could not load data ... ", request);
})
},
/** UK Met Office Specific Methods - These are not part of the default provider methods */
/*
* Gets the complete url for the request
*/
getUrl(forecastType) {
return this.config.apiBase + this.config.locationID + this.getParams(forecastType);
},
/*
* Generate a WeatherObject based on currentWeatherInformation
*/
generateWeatherObjectFromCurrentWeather(currentWeatherData) {
const currentWeather = new WeatherObject(this.config.units);
// data times are always UTC
let nowUtc = moment.utc()
let midnightUtc = nowUtc.clone().startOf("day")
let timeInMins = nowUtc.diff(midnightUtc, "minutes");
// loop round each of the (5) periods, look for today (the first period may be yesterday)
for (i in currentWeatherData.SiteRep.DV.Location.Period) {
let periodDate = moment.utc(currentWeatherData.SiteRep.DV.Location.Period[i].value.substr(0,10), "YYYY-MM-DD")
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
// check this is the period we want, after today the diff will be -ve
if (moment().diff(periodDate, "minutes") > 0) {
// loop round the reports looking for the one we are in
// $ value specifies the time in minutes-of-the-day: 0, 180, 360,...1260
for (j in currentWeatherData.SiteRep.DV.Location.Period[i].Rep){
let p = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].$;
if (timeInMins >= p && timeInMins-180 < p) {
// finally got the one we want, so populate weather object
currentWeather.humidity = currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].H;
currentWeather.temperature = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].T);
currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].F);
currentWeather.precipitation = parseInt(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].Pp);
currentWeather.windSpeed = this.convertWindSpeed(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].S);
currentWeather.windDirection = this.convertWindDirection(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].D);
currentWeather.weatherType = this.convertWeatherType(currentWeatherData.SiteRep.DV.Location.Period[i].Rep[j].W);
}
}
}
}
}
// determine the sunrise/sunset times - not supplied in UK Met Office data
let times = this.calcAstroData(currentWeatherData.SiteRep.DV.Location)
currentWeather.sunrise = times[0];
currentWeather.sunset = times[1];
return currentWeather;
},
/*
* Generate WeatherObjects based on forecast information
*/
generateWeatherObjectsFromForecast(forecasts) {
const days = [];
// loop round the (5) periods getting the data
// for each period array, Day is [0], Night is [1]
for (j in forecasts.SiteRep.DV.Location.Period) {
const weather = new WeatherObject(this.config.units);
// data times are always UTC
dateStr = forecasts.SiteRep.DV.Location.Period[j].value
let periodDate = moment.utc(dateStr.substr(0,10), "YYYY-MM-DD")
// ignore if period is before today
if (periodDate.isSameOrAfter(moment.utc().startOf("day"))) {
// populate the weather object
weather.date = moment.utc(dateStr.substr(0,10), "YYYY-MM-DD");
weather.minTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[1].Nm);
weather.maxTemperature = this.convertTemp(forecasts.SiteRep.DV.Location.Period[j].Rep[0].Dm);
weather.weatherType = this.convertWeatherType(forecasts.SiteRep.DV.Location.Period[j].Rep[0].W);
weather.precipitation = parseInt(forecasts.SiteRep.DV.Location.Period[j].Rep[0].PPd);
days.push(weather);
}
}
return days;
},
/*
* calculate the astronomical data
*/
calcAstroData(location) {
const sunTimes = [];
// determine the sunrise/sunset times
let times = SunCalc.getTimes(new Date(), location.lat, location.lon);
sunTimes.push(moment(times.sunrise, "X"));
sunTimes.push(moment(times.sunset, "X"));
return sunTimes;
},
/*
* Convert the Met Office icons to a more usable name.
*/
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;
},
/*
* Convert temp (from degrees C) if required
*/
convertTemp(tempInC) {
return this.units === "imperial" ? tempInC * 9 / 5 + 32 : tempInC;
},
/*
* Convert wind speed (from mph) if required
*/
convertWindSpeed(windInMph) {
return this.units === "metric" ? windInMph * 2.23694 : windInMph;
},
/*
* Convert the wind direction cardinal to value
*/
convertWindDirection(windDirection) {
const windCardinals = {
"N": 0,
"NNE": 22,
"NE": 45,
"ENE": 67,
"E": 90,
"ESE": 112,
"SE": 135,
"SSE": 157,
"S": 180,
"SSW": 202,
"SW": 225,
"WSW": 247,
"W": 270,
"WNW": 292,
"NW": 315,
"NNW": 337
};
return windCardinals.hasOwnProperty(windDirection) ? windCardinals[windDirection] : null;
},
/*
* Generates an url with api parameters based on the config.
*
* return String - URL params.
*/
getParams(forecastType) {
let params = "?";
params += "res=" + forecastType;
params += "&key=" + this.config.apiKey;
return params;
}
});

13
modules/default/weather/weather.js Normal file → Executable file
View File

@ -68,13 +68,14 @@ Module.register("weather",{
"moment.js",
"weatherprovider.js",
"weatherobject.js",
"suncalc.js",
this.file("providers/" + this.config.weatherProvider.toLowerCase() + ".js")
];
},
// Override getHeader method.
getHeader: function() {
if (this.config.appendLocationNameToHeader && this.weatherProvider) {
if (this.config.appendLocationNameToHeader && this.data.header !== undefined && this.weatherProvider) {
return this.data.header + " " + this.weatherProvider.fetchedLocation();
}
@ -188,11 +189,11 @@ Module.register("weather",{
this.nunjucksEnvironment().addFilter("unit", function (value, type) {
if (type === "temperature") {
if (this.config.units === "metric" || this.config.units === "imperial") {
if (this.config.units === "metric" || this.config.units === "imperial" || this.config.units === "ukunits") {
value += "°";
}
if (this.config.degreeLabel) {
if (this.config.units === "metric") {
if (this.config.units === "metric" || this.config.units === "ukunits") {
value += "C";
} else if (this.config.units === "imperial") {
value += "F";
@ -204,7 +205,11 @@ Module.register("weather",{
if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
value = "";
} else {
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
if (this.config.weatherProvider === "ukmetoffice") {
value += "%"
} else {
value = `${value.toFixed(2)} ${this.config.units === "imperial" ? "in" : "mm"}`;
}
}
} else if (type === "humidity") {
value += "%"

9
modules/default/weather/weatherobject.js Normal file → Executable file
View File

@ -28,6 +28,8 @@ class WeatherObject {
this.rain = null;
this.snow = null;
this.precipitation = null;
this.feelsLikeTemp = null;
}
cardinalWindDirection() {
@ -67,7 +69,7 @@ class WeatherObject {
}
beaufortWindSpeed() {
const windInKmh = this.units === "imperial" ? this.windSpeed * 1.609344 : this.windSpeed * 60 * 60 / 1000;
const windInKmh = (this.units === "imperial" || this.units === "ukunits") ? this.windSpeed * 1.609344 : this.windSpeed * 60 * 60 / 1000;
const speeds = [1, 5, 11, 19, 28, 38, 49, 61, 74, 88, 102, 117, 1000];
for (const [index, speed] of speeds.entries()) {
if (speed > windInKmh) {
@ -82,7 +84,10 @@ class WeatherObject {
}
feelsLike() {
const windInMph = this.units === "imperial" ? this.windSpeed : this.windSpeed * 2.23694;
if (this.feelsLikeTemp) {
return this.feelsLikeTemp
}
const windInMph = (this.units === "imperial" || this.units === "ukunits") ? this.windSpeed : this.windSpeed * 2.23694;
const tempInF = this.units === "imperial" ? this.temperature : this.temperature * 9 / 5 + 32;
let feelsLike = tempInF;

3491
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -46,11 +46,11 @@
"grunt-yamllint": "latest",
"http-auth": "^3.2.3",
"jsdom": "^11.6.2",
"jshint": "^2.9.5",
"jshint": "^2.10.2",
"mocha": "^4.1.0",
"mocha-each": "^1.1.0",
"spectron": "^3.8.0",
"stylelint": "^8.4.0",
"stylelint": "^9.10.1",
"stylelint-config-standard": "latest",
"time-grunt": "latest"
},

5
translations/en.json Normal file → Executable file
View File

@ -30,6 +30,7 @@
"UPDATE_NOTIFICATION_MODULE": "Update available for {MODULE_NAME} module.",
"UPDATE_INFO_SINGLE": "The current installation is {COMMIT_COUNT} commit behind on the {BRANCH_NAME} branch.",
"UPDATE_INFO_MULTIPLE": "The current installation is {COMMIT_COUNT} commits behind on the {BRANCH_NAME} branch.",
"FEELS": "Feels"
"FEELS": "Feels",
"PRECIP": "PoP"
}

30
vendor/package-lock.json generated vendored
View File

@ -703,7 +703,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
"integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
"optional": true,
"requires": {
"is-glob": "^2.0.0"
}
@ -717,8 +716,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"invert-kv": {
"version": "1.0.0",
@ -737,8 +735,7 @@
"is-buffer": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz",
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=",
"optional": true
"integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw="
},
"is-dotfile": {
"version": "1.0.3",
@ -764,8 +761,7 @@
"is-extglob": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
"optional": true
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
},
"is-fullwidth-code-point": {
"version": "1.0.0",
@ -779,7 +775,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@ -808,8 +803,7 @@
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"optional": true
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isobject": {
"version": "2.1.0",
@ -824,7 +818,6 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"optional": true,
"requires": {
"is-buffer": "^1.1.5"
}
@ -890,7 +883,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"optional": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
@ -1039,14 +1031,12 @@
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=",
"optional": true
"integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
},
"repeat-element": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz",
"integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=",
"optional": true
"integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo="
},
"repeat-string": {
"version": "1.6.1",
@ -1057,8 +1047,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"optional": true
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"set-immediate-shim": {
"version": "1.0.1",
@ -1093,6 +1082,11 @@
"ansi-regex": "^2.0.0"
}
},
"suncalc": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/suncalc/-/suncalc-1.8.0.tgz",
"integrity": "sha1-HZiYEJVjB4dQ9JlKlZ5lTYdqy/U="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

1
vendor/package.json vendored Normal file → Executable file
View File

@ -14,6 +14,7 @@
"moment": "^2.17.1",
"moment-timezone": "^0.5.11",
"nunjucks": "^3.0.1",
"suncalc": "^1.8.0",
"weathericons": "^2.1.0"
}
}

3
vendor/vendor.js vendored Normal file → Executable file
View File

@ -13,7 +13,8 @@ var vendor = {
"weather-icons.css": "node_modules/weathericons/css/weather-icons.css",
"weather-icons-wind.css": "node_modules/weathericons/css/weather-icons-wind.css",
"font-awesome.css": "css/font-awesome.css",
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js"
"nunjucks.js": "node_modules/nunjucks/browser/nunjucks.min.js",
"suncalc.js": "node_modules/suncalc/suncalc.js"
};
if (typeof module !== "undefined"){module.exports = vendor;}