Merge pull request #2081 from bryanzzhu/bryanzzhu-weather

adds current, hourly, and daily forecasts to the Weather module (OpenWeatherMap One Call API)
This commit is contained in:
Michael Teeuw 2020-09-18 12:28:20 +02:00 committed by GitHub
commit fd4576b234
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 258 additions and 9 deletions

View File

@ -22,6 +22,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
- Added eslint-plugin for jsdoc comments
### Updated

View File

@ -1,4 +1,7 @@
{% if current %}
{% if current or weatherData %}
{% if weatherData %}
{% set current = weatherData.current %}
{% endif %}
{% if not config.onlyTemp %}
<div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span>

View File

@ -1,5 +1,10 @@
{% if forecast %}
{% set numSteps = forecast | calcNumSteps %}
{% if forecast or weatherData %}
{% if weatherData %}
{% set forecast = weatherData.days %}
{% set numSteps = forecast | calcNumEntries %}
{% else %}
{% set numSteps = forecast | calcNumSteps %}
{% endif %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% set forecast = forecast.slice(0, numSteps) %}

View File

@ -0,0 +1,32 @@
{% if hourly or weatherData %}
{% if weatherData %}
{% set hourly = weatherData.hours %}
{% endif %}
{% set numSteps = hourly | calcNumEntries %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% set hours = hourly.slice(0, numSteps) %}
{% for hour in hours %}
<tr {% if config.colored %}class="colored"{% endif %} {% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
<td class="day">{{ hour.date | formatTime }}</td>
<td class="bright weather-icon"><span class="wi weathericon wi-{{ hour.weatherType }}"></span></td>
<td class="align-right bright">
{{ hour.temperature | roundValue | unit("temperature") }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation">
{{ hour.precipitation | unit("precip") }}
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">
{{ "LOADING" | translate | safe }}
</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{weatherData | dump}}</div> -->

View File

@ -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;

View File

@ -11,8 +11,10 @@ Module.register("weather", {
defaults: {
weatherProvider: "openweathermap",
roundTemp: false,
type: "current", //current, forecast
type: "current", // current, forecast, daily (equivalent to forecast), hourly (only with OpenWeatherMap /onecall endpoint)
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.
@ -125,7 +128,17 @@ Module.register("weather", {
// Select the template depending on the display type.
getTemplate: function () {
return `${this.config.type.toLowerCase()}.njk`;
switch (this.config.type.toLowerCase()) {
case "current":
return `current.njk`;
case "hourly":
return `hourly.njk`;
case "daily":
case "forecast":
return `forecast.njk`;
default:
return `${this.config.type.toLowerCase()}.njk`;
}
},
// Add all the data to the template.
@ -134,6 +147,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
@ -155,7 +169,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();
@ -207,7 +223,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") {
@ -245,6 +261,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) {

View File

@ -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;