diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f75d5e1..3db83f90 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ _This release is scheduled to be released on 2020-10-01._
- Add lithuanian language.
- Added support in weatherforecast for OpenWeather onecall API.
- Added config option to calendar-icons for recurring- and fullday-events
+- Added current, hourly (max 48), and daily (max 7) weather forecasts to weather module via OpenWeatherMap One Call API
### Updated
diff --git a/modules/default/weather/current.njk b/modules/default/weather/current.njk
index d89c5f53..c838c1e9 100755
--- a/modules/default/weather/current.njk
+++ b/modules/default/weather/current.njk
@@ -1,4 +1,7 @@
-{% if current %}
+{% if current or weatherData %}
+ {% if weatherData %}
+ {% set current = weatherData.current %}
+ {% endif %}
{% if not config.onlyTemp %}
diff --git a/modules/default/weather/forecast.njk b/modules/default/weather/forecast.njk
index fed0e85c..bd223101 100644
--- a/modules/default/weather/forecast.njk
+++ b/modules/default/weather/forecast.njk
@@ -1,4 +1,7 @@
-{% if forecast %}
+{% if forecast or weatherData %}
+ {% if weatherData %}
+ {% set forecast = weatherData.days %}
+ {% endif %}
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
diff --git a/modules/default/weather/hourly.njk b/modules/default/weather/hourly.njk
new file mode 100644
index 00000000..e241b010
--- /dev/null
+++ b/modules/default/weather/hourly.njk
@@ -0,0 +1,32 @@
+{% if hourly or weatherData %}
+ {% if weatherData %}
+ {% set hourly = weatherData.hours %}
+ {% endif %}
+ {% set numSteps = hourly | calcNumEntries %}
+ {% set currentStep = 0 %}
+
+ {% set hours = hourly.slice(0, numSteps) %}
+ {% for hour in hours %}
+
+ {{ hour.date | formatTime }} |
+ |
+
+ {{ hour.temperature | roundValue | unit("temperature") }}
+ |
+ {% if config.showPrecipitationAmount %}
+
+ {{ hour.precipitation | unit("precip") }}
+ |
+ {% endif %}
+
+ {% set currentStep = currentStep + 1 %}
+ {% endfor %}
+
+{% else %}
+
+ {{ "LOADING" | translate | safe }}
+
+{% endif %}
+
+
+
diff --git a/modules/default/weather/providers/openweathermap.js b/modules/default/weather/providers/openweathermap.js
index 70b715a0..414c23c0 100755
--- a/modules/default/weather/providers/openweathermap.js
+++ b/modules/default/weather/providers/openweathermap.js
@@ -35,7 +35,7 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable());
},
- // Overwrite the fetchCurrentWeather method.
+ // Overwrite the fetchWeatherForecast method.
fetchWeatherForecast() {
this.fetchData(this.getUrl())
.then((data) => {
@@ -56,6 +56,27 @@ WeatherProvider.register("openweathermap", {
.finally(() => this.updateAvailable());
},
+ // Overwrite the fetchWeatherData method.
+ fetchWeatherData() {
+ this.fetchData(this.getUrl())
+ .then((data) => {
+ if (!data) {
+ // Did not receive usable new data.
+ // Maybe this needs a better check?
+ return;
+ }
+
+ this.setFetchedLocation(`(${data.lat},${data.lon})`);
+
+ const weatherData = this.generateWeatherObjectsFromOnecall(data);
+ this.setWeatherData(weatherData);
+ })
+ .catch(function (request) {
+ Log.error("Could not load data ... ", request);
+ })
+ .finally(() => this.updateAvailable());
+ },
+
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
/*
* Gets the complete url for the request
@@ -95,6 +116,18 @@ WeatherProvider.register("openweathermap", {
return days;
},
+ /*
+ * Generate WeatherObjects based on One Call forecast information
+ */
+ generateWeatherObjectsFromOnecall(data) {
+ if (this.config.weatherEndpoint === "/onecall") {
+ return this.fetchOnecall(data);
+ }
+ // if weatherEndpoint does not match onecall, what should be returned?
+ const weatherData = {current: new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits), hours: [], days: []};
+ return weatherData;
+ },
+
/*
* fetch forecast information for 3-hourly forecast (available for free subscription).
*/
@@ -221,6 +254,129 @@ WeatherProvider.register("openweathermap", {
return days;
},
+ /*
+ * Fetch One Call forecast information (available for free subscription).
+ * Factors in timezone offsets.
+ * Minutely forecasts are excluded for the moment, see getParams().
+ */
+ fetchOnecall(data) {
+ let precip = false;
+
+ // get current weather, if requested
+ const current = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
+ if (data.hasOwnProperty("current")) {
+ current.date = moment(data.current.dt, "X").utcOffset(data.timezone_offset/60);
+ current.windSpeed = data.current.wind_speed;
+ current.windDirection = data.current.wind_deg;
+ current.sunrise = moment(data.current.sunrise, "X").utcOffset(data.timezone_offset/60);
+ current.sunset = moment(data.current.sunset, "X").utcOffset(data.timezone_offset/60);
+ current.temperature = data.current.temp;
+ current.weatherType = this.convertWeatherType(data.current.weather[0].icon);
+ current.humidity = data.current.humidity;
+ if (data.current.hasOwnProperty("rain") && !isNaN(data.current["rain"]["1h"])) {
+ if (this.config.units === "imperial") {
+ current.rain = data.current["rain"]["1h"] / 25.4;
+ } else {
+ current.rain = data.current["rain"]["1h"];
+ }
+ precip = true;
+ }
+ if (data.current.hasOwnProperty("snow") && !isNaN(data.current["snow"]["1h"])) {
+ if (this.config.units === "imperial") {
+ current.snow = data.current["snow"]["1h"] / 25.4;
+ } else {
+ current.snow = data.current["snow"]["1h"];
+ }
+ precip = true;
+ }
+ if (precip) {
+ current.precipitation = current.rain+current.snow;
+ }
+ current.feelsLikeTemp = data.current.feels_like;
+ }
+
+ let weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
+
+ // get hourly weather, if requested
+ const hours = [];
+ if (data.hasOwnProperty("hourly")) {
+ for (const hour of data.hourly) {
+ weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60);
+ // weather.date = moment(hour.dt, "X").utcOffset(data.timezone_offset/60).format(onecallDailyFormat+","+onecallHourlyFormat);
+ weather.temperature = hour.temp;
+ weather.feelsLikeTemp = hour.feels_like;
+ weather.humidity = hour.humidity;
+ weather.windSpeed = hour.wind_speed;
+ weather.windDirection = hour.wind_deg;
+ weather.weatherType = this.convertWeatherType(hour.weather[0].icon);
+ precip = false;
+ if (hour.hasOwnProperty("rain") && !isNaN(hour.rain["1h"])) {
+ if (this.config.units === "imperial") {
+ weather.rain = hour.rain["1h"] / 25.4;
+ } else {
+ weather.rain = hour.rain["1h"];
+ }
+ precip = true;
+ }
+ if (hour.hasOwnProperty("snow") && !isNaN(hour.snow["1h"])) {
+ if (this.config.units === "imperial") {
+ weather.snow = hour.snow["1h"] / 25.4;
+ } else {
+ weather.snow = hour.snow["1h"];
+ }
+ precip = true;
+ }
+ if (precip) {
+ weather.precipitation = weather.rain+weather.snow;
+ }
+
+ hours.push(weather);
+ weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
+ }
+ }
+
+ // get daily weather, if requested
+ const days = [];
+ if (data.hasOwnProperty("daily")) {
+ for (const day of data.daily) {
+ weather.date = moment(day.dt, "X").utcOffset(data.timezone_offset/60);
+ weather.sunrise = moment(day.sunrise, "X").utcOffset(data.timezone_offset/60);
+ weather.sunset = moment(day.sunset, "X").utcOffset(data.timezone_offset/60);
+ weather.minTemperature = day.temp.min;
+ weather.maxTemperature = day.temp.max;
+ weather.humidity = day.humidity;
+ weather.windSpeed = day.wind_speed;
+ weather.windDirection = day.wind_deg;
+ weather.weatherType = this.convertWeatherType(day.weather[0].icon);
+ precip = false;
+ if (!isNaN(day.rain)) {
+ if (this.config.units === "imperial") {
+ weather.rain = day.rain / 25.4;
+ } else {
+ weather.rain = day.rain;
+ }
+ precip = true;
+ }
+ if (!isNaN(day.snow)) {
+ if (this.config.units === "imperial") {
+ weather.snow = day.snow / 25.4;
+ } else {
+ weather.snow = day.snow;
+ }
+ precip = true;
+ }
+ if (precip) {
+ weather.precipitation = weather.rain+weather.snow;
+ }
+
+ days.push(weather);
+ weather = new WeatherObject(this.config.units, this.config.tempUnits, this.config.windUnits);
+ }
+ }
+
+ return {current: current, hours: hours, days: days};
+ },
+
/*
* Convert the OpenWeatherMap icons to a more usable name.
*/
@@ -256,7 +412,19 @@ WeatherProvider.register("openweathermap", {
*/
getParams() {
let params = "?";
- if (this.config.locationID) {
+ if (this.config.weatherEndpoint === "/onecall") {
+ params += "lat=" + this.config.lat;
+ params += "&lon=" + this.config.lon;
+ if (this.config.type === "current") {
+ params += "&exclude=minutely,hourly,daily";
+ } else if (this.config.type === "hourly") {
+ params += "&exclude=current,minutely,daily";
+ } else if (this.config.type === "daily" || this.config.type === "forecast") {
+ params += "&exclude=current,minutely,hourly";
+ } else {
+ params += "&exclude=minutely";
+ }
+ } else if (this.config.locationID) {
params += "id=" + this.config.locationID;
} else if (this.config.location) {
params += "q=" + this.config.location;
diff --git a/modules/default/weather/weather.js b/modules/default/weather/weather.js
index 73c493e0..83db10d2 100644
--- a/modules/default/weather/weather.js
+++ b/modules/default/weather/weather.js
@@ -11,8 +11,10 @@ Module.register("weather", {
defaults: {
weatherProvider: "openweathermap",
roundTemp: false,
- type: "current", //current, forecast
+ type: "current",
+ lat: 0,
+ lon: 0,
location: false,
locationID: false,
units: config.units,
@@ -36,6 +38,7 @@ Module.register("weather", {
showIndoorTemperature: false,
showIndoorHumidity: false,
maxNumberOfDays: 5,
+ maxEntries: 5,
fade: true,
fadePoint: 0.25, // Start on 1/4th of the list.
@@ -124,6 +127,9 @@ Module.register("weather", {
// Select the template depending on the display type.
getTemplate: function () {
+ if (this.config.type === "daily") {
+ return `forecast.njk`;
+ }
return `${this.config.type.toLowerCase()}.njk`;
},
@@ -133,6 +139,7 @@ Module.register("weather", {
config: this.config,
current: this.weatherProvider.currentWeather(),
forecast: this.weatherProvider.weatherForecast(),
+ weatherData: this.weatherProvider.weatherData(),
indoor: {
humidity: this.indoorHumidity,
temperature: this.indoorTemperature
@@ -154,7 +161,9 @@ Module.register("weather", {
}
setTimeout(() => {
- if (this.config.type === "forecast") {
+ if (this.config.weatherEndpoint === "/onecall") {
+ this.weatherProvider.fetchWeatherData();
+ } else if (this.config.type === "forecast") {
this.weatherProvider.fetchWeatherForecast();
} else {
this.weatherProvider.fetchCurrentWeather();
@@ -206,7 +215,7 @@ Module.register("weather", {
}
}
} else if (type === "precip") {
- if (isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
+ if (value === null || isNaN(value) || value === 0 || value.toFixed(2) === "0.00") {
value = "";
} else {
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
@@ -244,6 +253,13 @@ Module.register("weather", {
}.bind(this)
);
+ this.nunjucksEnvironment().addFilter(
+ "calcNumEntries",
+ function (dataArray) {
+ return Math.min(dataArray.length, this.config.maxEntries);
+ }.bind(this)
+ );
+
this.nunjucksEnvironment().addFilter(
"opacity",
function (currentStep, numSteps) {
diff --git a/modules/default/weather/weatherprovider.js b/modules/default/weather/weatherprovider.js
index cfcb3ef6..dd874be4 100644
--- a/modules/default/weather/weatherprovider.js
+++ b/modules/default/weather/weatherprovider.js
@@ -16,6 +16,7 @@ var WeatherProvider = Class.extend({
// Try to not access them directly.
currentWeatherObject: null,
weatherForecastArray: null,
+ weatherDataObject: null,
fetchedLocationName: null,
// The following properties will be set automatically.
@@ -56,6 +57,12 @@ var WeatherProvider = Class.extend({
Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherForecast method.`);
},
+ // This method should start the API request to fetch the weather forecast.
+ // This method should definitely be overwritten in the provider.
+ fetchWeatherData: function () {
+ Log.warn(`Weather provider: ${this.providerName} does not subclass the fetchWeatherData method.`);
+ },
+
// This returns a WeatherDay object for the current weather.
currentWeather: function () {
return this.currentWeatherObject;
@@ -66,6 +73,11 @@ var WeatherProvider = Class.extend({
return this.weatherForecastArray;
},
+ // This returns an object containing WeatherDay object(s) depending on the type of call.
+ weatherData: function () {
+ return this.weatherDataObject;
+ },
+
// This returns the name of the fetched location or an empty string.
fetchedLocation: function () {
return this.fetchedLocationName || "";
@@ -83,6 +95,11 @@ var WeatherProvider = Class.extend({
this.weatherForecastArray = weatherForecastArray;
},
+ // Set the weatherDataObject and notify the delegate that new information is available.
+ setWeatherData: function (weatherDataObject) {
+ this.weatherDataObject = weatherDataObject;
+ },
+
// Set the fetched location name.
setFetchedLocation: function (name) {
this.fetchedLocationName = name;