diff --git a/CHANGELOG.md b/CHANGELOG.md index be46a9ce..761e37b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ _This release is scheduled to be released on 2020-10-01._ - Fix calendar display. Account for current timezone. [#2068](https://github.com/MichMich/MagicMirror/issues/2068) - Fix logLevel being set before loading config. - Fix incorrect namespace links in svg clockfaces. [#2072](https://github.com/MichMich/MagicMirror/issues/2072) +- Fix weather/providers/weathergov for API guidelines [#2045] ## [2.12.0] - 2020-07-01 diff --git a/modules/default/weather/providers/weathergov.js b/modules/default/weather/providers/weathergov.js index c24e4de1..1b28bba1 100755 --- a/modules/default/weather/providers/weathergov.js +++ b/modules/default/weather/providers/weathergov.js @@ -3,76 +3,155 @@ /* Magic Mirror * Module: Weather * Provider: weather.gov + * https://weather-gov.github.io/api/general-faqs * - * By Vince Peri + * Original by Vince Peri * MIT Licensed. * * This class is a provider for weather.gov. * Note that this is only for US locations (lat and lon) and does not require an API key - * Since it is free, there are some items missing - like sunrise, sunset, humidity, etc. + * Since it is free, there are some items missing - like sunrise, sunset */ + WeatherProvider.register("weathergov", { // 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: "Weather.gov", + // Flag all needed URLs availability + configURLs: false, + + //This API has multiple urls involved + forecastURL: "tbd", + forecastHourlyURL: "tbd", + forecastGridDataURL: "tbd", + observationStationsURL: "tbd", + stationObsURL: "tbd", + + // Called to set the config, this config is the same as the weather module's config. + setConfig: function (config) { + this.config = config; + (this.config.apiBase = "https://api.weather.gov"), this.fetchWxGovURLs(this.config); + }, + + // Called when the weather provider is about to start. + start: function () { + Log.info(`Weather provider: ${this.providerName} started.`); + }, + + // This returns the name of the fetched location or an empty string. + fetchedLocation: function () { + return this.fetchedLocationName || ""; + }, + // Overwrite the fetchCurrentWeather method. fetchCurrentWeather() { - this.fetchData(this.getUrl()) + if (!this.configURLs) { + Log.info("fetch wx waiting on config URLs"); + return; + } + this.fetchData(this.stationObsURL) .then((data) => { - if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) { + if (!data || !data.properties) { // Did not receive usable new data. - // Maybe this needs a better check? return; } - - const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]); + const currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties); this.setCurrentWeather(currentWeather); }) .catch(function (request) { - Log.error("Could not load data ... ", request); + Log.error("Could not load station obs data ... ", request); }) .finally(() => this.updateAvailable()); }, - // Overwrite the fetchCurrentWeather method. + // Overwrite the fetchWeatherForecast method. fetchWeatherForecast() { - this.fetchData(this.getUrl()) + if (!this.configURLs) { + Log.info("fetch wx waiting on config URLs"); + return; + } + this.fetchData(this.forecastURL) .then((data) => { if (!data || !data.properties || !data.properties.periods || !data.properties.periods.length) { // Did not receive usable new data. - // Maybe this needs a better check? return; } - const forecast = this.generateWeatherObjectsFromForecast(data.properties.periods); this.setWeatherForecast(forecast); }) .catch(function (request) { - Log.error("Could not load data ... ", request); + Log.error("Could not load forecast hourly data ... ", request); }) .finally(() => this.updateAvailable()); }, /** Weather.gov Specific Methods - These are not part of the default provider methods */ + /* - * Gets the complete url for the request + * Get specific URLs */ - getUrl() { - return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint; + fetchWxGovURLs(config) { + this.fetchData(`${config.apiBase}/points/${config.lat},${config.lon}`) + .then((data) => { + if (!data || !data.properties) { + // points URL did not respond with usable data. + return; + } + this.fetchedLocationName = data.properties.relativeLocation.properties.city + ", " + data.properties.relativeLocation.properties.state; + Log.log("Forecast location is " + this.fetchedLocationName); + this.forecastURL = data.properties.forecast; + this.forecastHourlyURL = data.properties.forecastHourly; + this.forecastGridDataURL = data.properties.forecastGridData; + this.observationStationsURL = data.properties.observationStations; + // with this URL, we chain another promise for the station obs URL + return this.fetchData(data.properties.observationStations); + }) + .then((obsData) => { + if (!obsData || !obsData.features) { + // obs station URL did not respond with usable data. + return; + } + this.stationObsURL = obsData.features[0].id + "/observations/latest"; + }) + .catch((err) => { + Log.error(err); + }) + .finally(() => { + // excellent, let's fetch some actual wx data + this.configURLs = true; + this.fetchCurrentWeather(); + }); }, /* * Generate a WeatherObject based on currentWeatherInformation + * Weather.gov API uses specific units; API does not include choice of units + * ... object needs data in units based on config! */ generateWeatherObjectFromCurrentWeather(currentWeatherData) { const currentWeather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits); - currentWeather.temperature = currentWeatherData.temperature; - currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1); - currentWeather.windDirection = this.convertWindDirection(currentWeatherData.windDirection); - currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime); + currentWeather.date = moment(currentWeatherData.timestamp); + currentWeather.temperature = this.convertTemp(currentWeatherData.temperature.value); + currentWeather.windSpeed = this.covertSpeed(currentWeatherData.windSpeed.value); + currentWeather.windDirection = currentWeatherData.windDirection.value; + currentWeather.minTemperature = this.convertTemp(currentWeatherData.minTemperatureLast24Hours.value); + currentWeather.maxTemperature = this.convertTemp(currentWeatherData.maxTemperatureLast24Hours.value); + currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value); + currentWeather.rain = null; + currentWeather.snow = null; + currentWeather.precipitation = this.convertLength(currentWeatherData.precipitationLastHour.value); + currentWeather.feelsLikeTemp = this.convertTemp(currentWeatherData.heatIndex.value); + + let isDaytime = true; + if (currentWeatherData.icon.includes("day")) { + isDaytime = true; + } else { + isDaytime = false; + } + currentWeather.weatherType = this.convertWeatherType(currentWeatherData.textDescription, isDaytime); // determine the sunrise/sunset times - not supplied in weather.gov data let times = this.calcAstroData(this.config.lat, this.config.lon); @@ -124,7 +203,7 @@ WeatherProvider.register("weathergov", { // specify date weather.date = moment(forecast.startTime); - // If the first value of today is later than 17:00, we have an icon at least! + // use the forecast isDayTime attribute to help build the weatherType label weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime); } @@ -148,6 +227,34 @@ WeatherProvider.register("weathergov", { return days.slice(1); }, + /* + * Unit conversions + */ + // conversion to fahrenheit + convertTemp(temp) { + if (this.config.tempUnits === "imperial") { + return (9 / 5) * temp + 32; + } else { + return temp; + } + }, + // conversion to mph + covertSpeed(metSec) { + if (this.config.windUnits === "imperial") { + return metSec * 2.23694; + } else { + return metSec; + } + }, + // conversion to inches + convertLength(meters) { + if (this.config.units === "imperial") { + return meters * 39.3701; + } else { + return meters; + } + }, + /* * Calculate the astronomical data */