mirror of
https://github.com/MichMich/MagicMirror.git
synced 2025-06-27 19:53:36 +00:00
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:
commit
fd4576b234
@ -22,6 +22,7 @@ _This release is scheduled to be released on 2020-10-01._
|
|||||||
- Add lithuanian language.
|
- Add lithuanian language.
|
||||||
- Added support in weatherforecast for OpenWeather onecall API.
|
- Added support in weatherforecast for OpenWeather onecall API.
|
||||||
- Added config option to calendar-icons for recurring- and fullday-events
|
- 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
|
- Added eslint-plugin for jsdoc comments
|
||||||
|
|
||||||
### Updated
|
### Updated
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{% if current %}
|
{% if current or weatherData %}
|
||||||
|
{% if weatherData %}
|
||||||
|
{% set current = weatherData.current %}
|
||||||
|
{% endif %}
|
||||||
{% if not config.onlyTemp %}
|
{% if not config.onlyTemp %}
|
||||||
<div class="normal medium">
|
<div class="normal medium">
|
||||||
<span class="wi wi-strong-wind dimmed"></span>
|
<span class="wi wi-strong-wind dimmed"></span>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
{% if forecast %}
|
{% if forecast or weatherData %}
|
||||||
{% set numSteps = forecast | calcNumSteps %}
|
{% if weatherData %}
|
||||||
|
{% set forecast = weatherData.days %}
|
||||||
|
{% set numSteps = forecast | calcNumEntries %}
|
||||||
|
{% else %}
|
||||||
|
{% set numSteps = forecast | calcNumSteps %}
|
||||||
|
{% endif %}
|
||||||
{% set currentStep = 0 %}
|
{% set currentStep = 0 %}
|
||||||
<table class="{{ config.tableClass }}">
|
<table class="{{ config.tableClass }}">
|
||||||
{% set forecast = forecast.slice(0, numSteps) %}
|
{% set forecast = forecast.slice(0, numSteps) %}
|
||||||
|
32
modules/default/weather/hourly.njk
Normal file
32
modules/default/weather/hourly.njk
Normal 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> -->
|
@ -35,7 +35,7 @@ WeatherProvider.register("openweathermap", {
|
|||||||
.finally(() => this.updateAvailable());
|
.finally(() => this.updateAvailable());
|
||||||
},
|
},
|
||||||
|
|
||||||
// Overwrite the fetchCurrentWeather method.
|
// Overwrite the fetchWeatherForecast method.
|
||||||
fetchWeatherForecast() {
|
fetchWeatherForecast() {
|
||||||
this.fetchData(this.getUrl())
|
this.fetchData(this.getUrl())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@ -56,6 +56,27 @@ WeatherProvider.register("openweathermap", {
|
|||||||
.finally(() => this.updateAvailable());
|
.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 */
|
/** OpenWeatherMap Specific Methods - These are not part of the default provider methods */
|
||||||
/*
|
/*
|
||||||
* Gets the complete url for the request
|
* Gets the complete url for the request
|
||||||
@ -95,6 +116,18 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return days;
|
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).
|
* fetch forecast information for 3-hourly forecast (available for free subscription).
|
||||||
*/
|
*/
|
||||||
@ -221,6 +254,129 @@ WeatherProvider.register("openweathermap", {
|
|||||||
return days;
|
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.
|
* Convert the OpenWeatherMap icons to a more usable name.
|
||||||
*/
|
*/
|
||||||
@ -256,7 +412,19 @@ WeatherProvider.register("openweathermap", {
|
|||||||
*/
|
*/
|
||||||
getParams() {
|
getParams() {
|
||||||
let params = "?";
|
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;
|
params += "id=" + this.config.locationID;
|
||||||
} else if (this.config.location) {
|
} else if (this.config.location) {
|
||||||
params += "q=" + this.config.location;
|
params += "q=" + this.config.location;
|
||||||
|
@ -11,8 +11,10 @@ Module.register("weather", {
|
|||||||
defaults: {
|
defaults: {
|
||||||
weatherProvider: "openweathermap",
|
weatherProvider: "openweathermap",
|
||||||
roundTemp: false,
|
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,
|
location: false,
|
||||||
locationID: false,
|
locationID: false,
|
||||||
units: config.units,
|
units: config.units,
|
||||||
@ -36,6 +38,7 @@ Module.register("weather", {
|
|||||||
showIndoorTemperature: false,
|
showIndoorTemperature: false,
|
||||||
showIndoorHumidity: false,
|
showIndoorHumidity: false,
|
||||||
maxNumberOfDays: 5,
|
maxNumberOfDays: 5,
|
||||||
|
maxEntries: 5,
|
||||||
fade: true,
|
fade: true,
|
||||||
fadePoint: 0.25, // Start on 1/4th of the list.
|
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.
|
// Select the template depending on the display type.
|
||||||
getTemplate: function () {
|
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.
|
// Add all the data to the template.
|
||||||
@ -134,6 +147,7 @@ Module.register("weather", {
|
|||||||
config: this.config,
|
config: this.config,
|
||||||
current: this.weatherProvider.currentWeather(),
|
current: this.weatherProvider.currentWeather(),
|
||||||
forecast: this.weatherProvider.weatherForecast(),
|
forecast: this.weatherProvider.weatherForecast(),
|
||||||
|
weatherData: this.weatherProvider.weatherData(),
|
||||||
indoor: {
|
indoor: {
|
||||||
humidity: this.indoorHumidity,
|
humidity: this.indoorHumidity,
|
||||||
temperature: this.indoorTemperature
|
temperature: this.indoorTemperature
|
||||||
@ -155,7 +169,9 @@ Module.register("weather", {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this.config.type === "forecast") {
|
if (this.config.weatherEndpoint === "/onecall") {
|
||||||
|
this.weatherProvider.fetchWeatherData();
|
||||||
|
} else if (this.config.type === "forecast") {
|
||||||
this.weatherProvider.fetchWeatherForecast();
|
this.weatherProvider.fetchWeatherForecast();
|
||||||
} else {
|
} else {
|
||||||
this.weatherProvider.fetchCurrentWeather();
|
this.weatherProvider.fetchCurrentWeather();
|
||||||
@ -207,7 +223,7 @@ Module.register("weather", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === "precip") {
|
} 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 = "";
|
value = "";
|
||||||
} else {
|
} else {
|
||||||
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
if (this.config.weatherProvider === "ukmetoffice" || this.config.weatherProvider === "ukmetofficedatahub") {
|
||||||
@ -245,6 +261,13 @@ Module.register("weather", {
|
|||||||
}.bind(this)
|
}.bind(this)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.nunjucksEnvironment().addFilter(
|
||||||
|
"calcNumEntries",
|
||||||
|
function (dataArray) {
|
||||||
|
return Math.min(dataArray.length, this.config.maxEntries);
|
||||||
|
}.bind(this)
|
||||||
|
);
|
||||||
|
|
||||||
this.nunjucksEnvironment().addFilter(
|
this.nunjucksEnvironment().addFilter(
|
||||||
"opacity",
|
"opacity",
|
||||||
function (currentStep, numSteps) {
|
function (currentStep, numSteps) {
|
||||||
|
@ -16,6 +16,7 @@ var WeatherProvider = Class.extend({
|
|||||||
// Try to not access them directly.
|
// Try to not access them directly.
|
||||||
currentWeatherObject: null,
|
currentWeatherObject: null,
|
||||||
weatherForecastArray: null,
|
weatherForecastArray: null,
|
||||||
|
weatherDataObject: null,
|
||||||
fetchedLocationName: null,
|
fetchedLocationName: null,
|
||||||
|
|
||||||
// The following properties will be set automatically.
|
// 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.`);
|
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.
|
// This returns a WeatherDay object for the current weather.
|
||||||
currentWeather: function () {
|
currentWeather: function () {
|
||||||
return this.currentWeatherObject;
|
return this.currentWeatherObject;
|
||||||
@ -66,6 +73,11 @@ var WeatherProvider = Class.extend({
|
|||||||
return this.weatherForecastArray;
|
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.
|
// This returns the name of the fetched location or an empty string.
|
||||||
fetchedLocation: function () {
|
fetchedLocation: function () {
|
||||||
return this.fetchedLocationName || "";
|
return this.fetchedLocationName || "";
|
||||||
@ -83,6 +95,11 @@ var WeatherProvider = Class.extend({
|
|||||||
this.weatherForecastArray = weatherForecastArray;
|
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.
|
// Set the fetched location name.
|
||||||
setFetchedLocation: function (name) {
|
setFetchedLocation: function (name) {
|
||||||
this.fetchedLocationName = name;
|
this.fetchedLocationName = name;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user