diff --git a/CHANGELOG.md b/CHANGELOG.md index 568333c0..6dad485a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Added fade, fadePoint and maxNumberOfDays properties to the forecast mode [#1516](https://github.com/MichMich/MagicMirror/issues/1516) - Fixed Loading string and decimalSymbol string replace [#1538](https://github.com/MichMich/MagicMirror/issues/1538) - Show Snow amounts in new weather module [#1545](https://github.com/MichMich/MagicMirror/issues/1545) +- Added weather.gov as a new weather provider for US locations ## [2.6.0] - 2019-01-01 diff --git a/modules/default/weather/README.md b/modules/default/weather/README.md index d15a208f..1b406958 100644 --- a/modules/default/weather/README.md +++ b/modules/default/weather/README.md @@ -35,21 +35,21 @@ The following properties can be configured: | Option | Description | ---------------------------- | ----------- -| `weatherProvider` | Which weather provider should be used.

**Possible values:** `openweathermap` and `darksky`
**Default value:** `openweathermap` -| `type` | Which type of weather data should be displayed.

**Possible values:** `current` and `forecast`
**Default value:** `current` -| `units` | What units to use. Specified by config.js

**Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` =Fahrenheit
**Default value:** `config.units` +| `weatherProvider` | Which weather provider should be used.

**Possible values:** `openweathermap` , `darksky` , or `weathergov`
**Default value:** `openweathermap` +| `type` | Which type of weather data should be displayed.

**Possible values:** `current` or `forecast`
**Default value:** `current` +| `units` | What units to use. Specified by config.js

**Possible values:** `config.units` = Specified by config.js, `default` = Kelvin, `metric` = Celsius, `imperial` = Fahrenheit
**Default value:** `config.units` | `roundTemp` | Round temperature value to nearest integer.

**Possible values:** `true` (round to integer) or `false` (display exact value with decimal point)
**Default value:** `false` -| `degreeLabel` | Show the degree label for your chosen units (Metric = C, Imperial = F, Kelvins = K).

**Possible values:** `true` or `false`
**Default value:** `false` +| `degreeLabel` | Show the degree label for your chosen units (Metric = C, Imperial = F, Kelvin = K).

**Possible values:** `true` or `false`
**Default value:** `false` | `updateInterval` | How often does the content needs to be fetched? (Milliseconds)

**Possible values:** `1000` - `86400000`
**Default value:** `600000` (10 minutes) -| `animationSpeed` | Speed of the update animation. (Milliseconds)

**Possible values:**`0` - `5000`
**Default value:** `1000` (1 second) +| `animationSpeed` | Speed of the update animation. (Milliseconds)

**Possible values:** `0` - `5000`
**Default value:** `1000` (1 second) | `timeFormat` | Use 12 or 24 hour format.

**Possible values:** `12` or `24`
**Default value:** uses value of _config.timeFormat_ | `showPeriod` | Show the period (am/pm) with 12 hour format

**Possible values:** `true` or `false`
**Default value:** `true` | `showPeriodUpper` | Show the period (AM/PM) with 12 hour format as uppercase

**Possible values:** `true` or `false`
**Default value:** `false` | `lang` | The language of the days.

**Possible values:** `en`, `nl`, `ru`, etc ...
**Default value:** uses value of _config.language_ | `decimalSymbol` | The decimal symbol to use.

**Possible values:** `.`, `,` or any other symbol.
**Default value:** `.` -| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds)

**Possible values:** `1000` - `5000`
**Default value:** `0` -| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly intresting when using calender based weather.

**Default value:** `true` -| `calendarClass` | The class for the calender module to base the event based weather information on.

**Default value:** `'calendar'` +| `initialLoadDelay` | The initial delay before loading. If you have multiple modules that use the same API key, you might want to delay one of the requests. (Milliseconds)

**Possible values:** `1000` - `5000`
**Default value:** `0` +| `appendLocationNameToHeader` | If set to `true`, the returned location name will be appended to the header of the module, if the header is enabled. This is mainly interesting when using calender based weather.

**Default value:** `true` +| `calendarClass` | The class for the calender module to base the event based weather information on.

**Default value:** `'calendar'` #### Current weather options @@ -62,14 +62,14 @@ The following properties can be configured: | `showHumidity` | Show the current humidity

**Possible values:** `true` or `false`
**Default value:** `false` | `showIndoorTemperature` | If you have another module that emits the `INDOOR_TEMPERATURE` notification, the indoor temperature will be displayed
**Default value:** `false` | `showIndoorHumidity` | If you have another module that emits the `INDOOR_HUMIDITY` notification, the indoor humidity will be displayed
**Default value:** `false` -| `showFeelsLike` | Shows the Feels like temperature weather.

**Possible values:**`true` or `false`
**Default value:** `true` +| `showFeelsLike` | Shows the Feels like temperature weather.

**Possible values:** `true` or `false`
**Default value:** `true` #### Weather forecast options | Option | Description | ---------------------------- | ----------- -| `tableClass` | The class for the forecast table.

**Default value:** `'small'` -| `colored` | If set to `true`, the min and max temperature are color coded.

**Default value:** `false` +| `tableClass` | The class for the forecast table.

**Default value:** `'small'` +| `colored` | If set to `true`, the min and max temperature are color coded.

**Default value:** `false` | `showPrecipitationAmount` | Show the amount of rain/snow in the forecast

**Possible values:** `true` or `false`
**Default value:** `false` | `fade` | Fade the future events to black. (Gradient)

**Possible values:** `true` or `false`
**Default value:** `true` | `fadePoint` | Where to start fade?

**Possible values:** `0` (top of the list) - `1` (bottom of list)
**Default value:** `0.25` @@ -79,22 +79,31 @@ The following properties can be configured: | Option | Description | ---------------------------- | ----------- -| `apiVersion` | The OpenWeatherMap API version to use.

**Default value:** `2.5` -| `apiBase` | The OpenWeatherMap base URL.

**Default value:** `'http://api.openweathermap.org/data/'` -| `weatherEndpoint` | The OpenWeatherMap API endPoint.

**Possible values:** `/weather`, `/forecast` (free users) or `/forecast/daily` (paying users or old apiKey only)
**Default value:** `'/weather'` +| `apiVersion` | The OpenWeatherMap API version to use.

**Default value:** `2.5` +| `apiBase` | The OpenWeatherMap base URL.

**Default value:** `'http://api.openweathermap.org/data/'` +| `weatherEndpoint` | The OpenWeatherMap API endPoint.

**Possible values:** `/weather`, `/forecast` (free users) or `/forecast/daily` (paying users or old apiKey only)
**Default value:** `'/weather'` | `locationID` | Location ID from [OpenWeatherMap](https://openweathermap.org/find) **This will override anything you put in location.**
Leave blank if you want to use location.
**Example:** `1234567`
**Default value:** `false`

**Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used. | `location` | The location used for weather information.

**Example:** `'Amsterdam,Netherlands'`
**Default value:** `false`

**Note:** When the `location` and `locationID` are both not set, the location will be based on the information provided by the calendar module. The first upcoming event with location data will be used. -| `apiKey` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account.

This value is **REQUIRED** +| `apiKey` | The [OpenWeatherMap](https://home.openweathermap.org) API key, which can be obtained by creating an OpenWeatherMap account.

This value is **REQUIRED** ### Darksky options | Option | Description | ---------------------------- | ----------- -| `apiBase` | The DarkSky base URL. The darksky api has disabled [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), therefore a proxy is required.

**Possible value:** `'https://cors-anywhere.herokuapp.com/https://api.darksky.net'`
This value is **REQUIRED** -| `weatherEndpoint` | The DarkSky API endPoint.

**Possible values:** `/forecast`
This value is **REQUIRED** -| `apiKey` | The [DarkSky](https://darksky.net/dev/register) API key, which can be obtained by creating an DarkSky account.

This value is **REQUIRED** -| `lat` | The geo coordinate latitude.

This value is **REQUIRED** -| `lon` | The geo coordinate longitude.

This value is **REQUIRED** +| `apiBase` | The DarkSky base URL. The darksky api has disabled [cors](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), therefore a proxy is required.

**Possible value:** `'https://cors-anywhere.herokuapp.com/https://api.darksky.net'`
This value is **REQUIRED** +| `weatherEndpoint` | The DarkSky API endPoint.

**Possible values:** `/forecast`
This value is **REQUIRED** +| `apiKey` | The [DarkSky](https://darksky.net/dev/register) API key, which can be obtained by creating an DarkSky account.

This value is **REQUIRED** +| `lat` | The geo coordinate latitude.

This value is **REQUIRED** +| `lon` | The geo coordinate longitude.

This value is **REQUIRED** + +### Weather.gov options + +| Option | Description +| ---------------------------- | ----------- +| `apiBase` | The weather.gov base URL.

**Possible value:** `'https://api.weather.gov/points/'`
This value is **REQUIRED** +| `weatherEndpoint` | The weather.gov API endPoint.

**Possible values:** `/forecast` for forecast and `/forecast/hourly` for current.
This value is **REQUIRED** +| `lat` | The geo coordinate latitude.

This value is **REQUIRED** +| `lon` | The geo coordinate longitude.

This value is **REQUIRED** ## API Provider Development diff --git a/modules/default/weather/providers/README.md b/modules/default/weather/providers/README.md index 85d9c3c5..7acb7e34 100644 --- a/modules/default/weather/providers/README.md +++ b/modules/default/weather/providers/README.md @@ -85,7 +85,7 @@ Notify the delegate that new weather is available. #### `fetchData(url, method, data)` -A convinience function to make requests. It returns a promise. +A convenience function to make requests. It returns a promise. ### WeatherObject diff --git a/modules/default/weather/providers/openweathermap.js b/modules/default/weather/providers/openweathermap.js index e388e278..f7fe0edc 100644 --- a/modules/default/weather/providers/openweathermap.js +++ b/modules/default/weather/providers/openweathermap.js @@ -103,13 +103,13 @@ WeatherProvider.register("openweathermap", { // initial variable declaration const days = []; // variables for temperature range and rain - var minTemp = []; - var maxTemp = []; - var rain = 0; - var snow = 0; + let minTemp = []; + let maxTemp = []; + let rain = 0; + let snow = 0; // variable for date let date = ""; - var weather = new WeatherObject(this.config.units); + let weather = new WeatherObject(this.config.units); for (const forecast of forecasts) { diff --git a/modules/default/weather/providers/weathergov.js b/modules/default/weather/providers/weathergov.js new file mode 100644 index 00000000..1110c68e --- /dev/null +++ b/modules/default/weather/providers/weathergov.js @@ -0,0 +1,256 @@ +/* global WeatherProvider, WeatherObject */ + +/* Magic Mirror + * Module: Weather + * Provider: weather.gov + * + * 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. + */ + +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", + + // Overwrite the fetchCurrentWeather method. + fetchCurrentWeather() { + this.fetchData(this.getUrl()) + .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 currentWeather = this.generateWeatherObjectFromCurrentWeather(data.properties.periods[0]); + this.setCurrentWeather(currentWeather); + }) + .catch(function(request) { + Log.error("Could not load data ... ", request); + }) + }, + + // Overwrite the fetchCurrentWeather method. + fetchWeatherForecast() { + this.fetchData(this.getUrl()) + .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); + }) + }, + + /** Weather.gov Specific Methods - These are not part of the default provider methods */ + /* + * Gets the complete url for the request + */ + getUrl() { + return this.config.apiBase + this.config.lat + "," + this.config.lon + this.config.weatherEndpoint; + }, + + /* + * Generate a WeatherObject based on currentWeatherInformation + */ + generateWeatherObjectFromCurrentWeather(currentWeatherData) { + const currentWeather = new WeatherObject(this.config.units); + + currentWeather.temperature = currentWeatherData.temperature; + currentWeather.windSpeed = currentWeatherData.windSpeed.split(" ", 1); + currentWeather.windDirection = this.convertDirectiontoDegrees(currentWeatherData.windDirection); + currentWeather.weatherType = this.convertWeatherType(currentWeatherData.shortForecast, currentWeatherData.isDaytime); + + return currentWeather; + }, + + /* + * Generate WeatherObjects based on forecast information + */ + generateWeatherObjectsFromForecast(forecasts) { + return this.fetchForecastDaily(forecasts); + }, + + /* + * fetch forecast information for daily forecast. + */ + fetchForecastDaily(forecasts) { + // initial variable declaration + const days = []; + // variables for temperature range and rain + let minTemp = []; + let maxTemp = []; + // variable for date + let date = ""; + let weather = new WeatherObject(this.config.units); + weather.precipitation = 0; + + for (const forecast of forecasts) { + + if (date !== moment(forecast.startTime).format("YYYY-MM-DD")) { + + // calculate minimum/maximum temperature, specify rain amount + weather.minTemperature = Math.min.apply(null, minTemp); + weather.maxTemperature = Math.max.apply(null, maxTemp); + + // push weather information to days array + days.push(weather); + // create new weather-object + weather = new WeatherObject(this.config.units); + + minTemp = []; + maxTemp = []; + weather.precipitation = 0; + + // set new date + date = moment(forecast.startTime).format("YYYY-MM-DD"); + + // specify date + weather.date = moment(forecast.startTime); + + // If the first value of today is later than 17:00, we have an icon at least! + weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime); + } + + if (moment(forecast.startTime).format("H") >= 8 && moment(forecast.startTime).format("H") <= 17) { + weather.weatherType = this.convertWeatherType(forecast.shortForecast, forecast.isDaytime); + } + + // the same day as before + // add values from forecast to corresponding variables + minTemp.push(forecast.temperature); + maxTemp.push(forecast.temperature); + } + + // last day + // calculate minimum/maximum temperature, specify rain amount + weather.minTemperature = Math.min.apply(null, minTemp); + weather.maxTemperature = Math.max.apply(null, maxTemp); + + // push weather information to days array + days.push(weather); + return days.slice(1); + }, + + /* + * Convert the icons to a more usable name. + */ + convertWeatherType(weatherType, isDaytime) { + //https://w1.weather.gov/xml/current_obs/weather.php + // There are way too many types to create, so lets just look for certain strings + + if (weatherType.includes("Cloudy") || weatherType.includes("Partly")) { + if (isDaytime) { + return "day-cloudy"; + } + + return "night-cloudy"; + } else if (weatherType.includes("Overcast")) { + if (isDaytime) { + return "cloudy"; + } + + return "night-cloudy"; + } else if (weatherType.includes("Freezing") || weatherType.includes("Ice")) { + return "rain-mix"; + } else if (weatherType.includes("Snow")) { + if (isDaytime) { + return "snow"; + } + + return "night-snow"; + } else if (weatherType.includes("Thunderstorm")) { + if (isDaytime) { + return "thunderstorm"; + } + + return "night-thunderstorm"; + } else if (weatherType.includes("Showers")) { + if (isDaytime) { + return "showers"; + } + + return "night-showers"; + } else if (weatherType.includes("Rain") || weatherType.includes("Drizzle")) { + if (isDaytime) { + return "rain"; + } + + return "night-rain"; + } else if (weatherType.includes("Breezy") || weatherType.includes("Windy")) { + if (isDaytime) { + return "cloudy-windy"; + } + + return "night-alt-cloudy-windy"; + } else if (weatherType.includes("Fair") || weatherType.includes("Clear") || weatherType.includes("Few") || weatherType.includes("Sunny")) { + if (isDaytime) { + return "day-sunny"; + } + + return "night-clear"; + } else if (weatherType.includes("Dust") || weatherType.includes("Sand")) { + return "dust"; + } else if (weatherType.includes("Fog")) { + return "fog"; + } else if (weatherType.includes("Smoke")) { + return "smoke"; + } else if (weatherType.includes("Haze")) { + return "day-haze"; + } + + return null; + }, + + /* + Convert the direction into Degrees + */ + convertDirectiontoDegrees(direction) { + if (direction === "NNE"){ + return 33.75; + } else if (direction === "NE") { + return 56.25; + } else if (direction === "ENE") { + return 78.75; + } else if (direction === "E") { + return 101.25; + } else if (direction === "ESE") { + return 123.75; + } else if (direction === "SE") { + return 146.25; + } else if (direction === "SSE") { + return 168.75; + } else if (direction === "S") { + return 191.25; + } else if (direction === "SSW") { + return 213.75; + } else if (direction === "SW") { + return 236.25; + } else if (direction === "WSW") { + return 258.75; + } else if (direction === "W") { + return 281.25; + } else if (direction === "WNW") { + return 303.75; + } else if (direction === "NW") { + return 326.25; + } else if (direction === "NNW") { + return 348.75; + } else { + return 0; + } + } +});