Release 2.33.0 (#3903)

This commit is contained in:
Kristjan ESPERANTO
2025-09-30 18:02:22 +02:00
committed by GitHub
parent 62b0f7f26e
commit b0c5924019
77 changed files with 2811 additions and 2654 deletions

View File

@@ -1,101 +1,97 @@
{% macro humidity() %}
{% if current.humidity %}
<span class="humidity"><span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup></span>
{% endif %}
{% if current.humidity %}
<span class="humidity"
><span>{{ current.humidity | decimalSymbol }}</span><sup>&nbsp;<i class="wi wi-humidity humidity-icon"></i></sup
></span>
{% endif %}
{% endmacro %}
{% if current %}
{% if not config.onlyTemp %}
<div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span>
<span>
{{ current.windSpeed | unit("wind") | round }}
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
&nbsp;
</sup>
{% endif %}
</span>
{% if config.showHumidity === "wind" %}
{{ humidity() }}
{% if not config.onlyTemp %}
<div class="normal medium">
<span class="wi wi-strong-wind dimmed"></span>
<span>
{{ current.windSpeed | unit("wind") | round }}
{% if config.showWindDirection %}
<sup>
{% if config.showWindDirectionAsArrow %}
<i class="fas fa-long-arrow-alt-down" style="transform:rotate({{ current.windFromDirection }}deg)"></i>
{% else %}
{{ current.cardinalWindDirection() | translate }}
{% endif %}
{% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
<span>
{% if current.nextSunAction() === "sunset" %}
{{ current.sunset | formatTime }}
{% else %}
{{ current.sunrise | formatTime }}
{% endif %}
</span>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
<div class="wi dimmed wi-hot"></div>
{{ current.uv_index }}
</td>
{% endif %}
</div>
{% endif %}
<div class="large">
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
<span class="medium fas fa-home"></span>
<span style="display: inline-block">
{% if config.showIndoorTemperature and indoor.temperature %}
<sup class="small" style="position: relative; display: block; text-align: left;">
<span>
{{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }}
</span>
</sup>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<sub class="small" style="position: relative; display: block; text-align: left;">
<span>
{{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }}
</span>
</sub>
{% endif %}
</span>
{% endif %}
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
{% if config.showHumidity === "temp" %}
<span class="medium bright">{{ humidity() }}</span>
&nbsp;
</sup>
{% endif %}
</span>
{% if config.showHumidity === "wind" %}
{{ humidity() }}
{% endif %}
{% if config.showSun %}
<span class="wi dimmed wi-{{ current.nextSunAction() }}"></span>
<span>
{% if current.nextSunAction() === "sunset" %}
{{ current.sunset | formatTime }}
{% else %}
{{ current.sunrise | formatTime }}
{% endif %}
</span>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right bright uv-index">
<div class="wi dimmed wi-hot"></div>
{{ current.uv_index }}
</td>
{% endif %}
</div>
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
<div class="normal medium feelslike">
{% if config.showFeelsLike %}
<span class="dimmed">
{% if config.showHumidity === "feelslike" %}
{{ humidity() }}
{% endif %}
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span>
<br />
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }}
</span>
<br />
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed">
<span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}%
</span>
{% endif %}
</div>
{% endif %}
<div class="flex large type-temp">
{% if config.showIndoorTemperature and indoor.temperature or config.showIndoorHumidity and indoor.humidity %}
<span class="medium fas fa-home"></span>
<span style="display: inline-block">
{% if config.showIndoorTemperature and indoor.temperature %}
<sup class="small" style="position: relative; display: block; text-align: left;">
<span> {{ indoor.temperature | roundValue | unit("temperature") | decimalSymbol }} </span>
</sup>
{% endif %}
{% if config.showIndoorHumidity and indoor.humidity %}
<sub class="small" style="position: relative; display: block; text-align: left;">
<span> {{ indoor.humidity | roundValue | unit("humidity") | decimalSymbol }} </span>
</sub>
{% endif %}
</span>
{% endif %}
{% if config.showHumidity === "below" %}
<span class="medium dimmed">{{ humidity() }}</span>
{% if current.weatherType %}
<span class="light wi weathericon wi-{{ current.weatherType }}"></span>
{% endif %}
<span class="light bright">{{ current.temperature | roundValue | unit("temperature") | decimalSymbol }}</span>
{% if config.showHumidity === "temp" %}
<span class="medium bright">{{ humidity() }}</span>
{% endif %}
</div>
{% if (config.showFeelsLike or config.showPrecipitationAmount or config.showPrecipitationProbability) and not config.onlyTemp %}
<div class="normal medium feelslike">
{% if config.showFeelsLike %}
<span class="dimmed">
{% if config.showHumidity === "feelslike" %}
{{ humidity() }}
{% endif %}
{{ "FEELS" | translate({DEGREE: current.feelsLike() | roundValue | unit("temperature") | decimalSymbol }) }}
</span>
<br />
{% endif %}
{% if config.showPrecipitationAmount and current.precipitationAmount %}
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_AMOUNT" | translate }}</span> {{ current.precipitationAmount | unit("precip", current.precipitationUnits) }} </span>
<br />
{% endif %}
{% if config.showPrecipitationProbability and current.precipitationProbability %}
<span class="dimmed"> <span class="precipitationLeadText">{{ "PRECIP_POP" | translate }}</span> {{ current.precipitationProbability }}% </span>
{% endif %}
</div>
{% endif %}
{% if config.showHumidity === "below" %}
<span class="medium dimmed">{{ humidity() }}</span>
{% endif %}
{% else %}
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `current` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{current | dump}}</div> -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ current | dump }}</div> -->

View File

@@ -1,52 +1,46 @@
{% if forecast %}
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% if config.ignoreToday %}
{% set forecast = forecast.splice(1) %}
{% set numSteps = forecast | calcNumSteps %}
{% set currentStep = 0 %}
<table class="{{ config.tableClass }}">
{% if config.ignoreToday %}
{% set forecast = forecast.splice(1) %}
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr
{% if config.colored %}class="colored"{% endif %}
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}
>
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format("ddd") }}</td>
{% endif %}
{% set forecast = forecast.slice(0, numSteps) %}
{% for f in forecast %}
<tr {% if config.colored %}class="colored"{% endif %}
{% if config.fade %}style="opacity: {{ currentStep | opacity(numSteps) }};"{% endif %}>
{% if (currentStep == 0) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TODAY" | translate }}</td>
{% elif (currentStep == 1) and config.ignoreToday == false and config.absoluteDates == false %}
<td class="day">{{ "TOMORROW" | translate }}</td>
{% else %}
<td class="day">{{ f.date.format("ddd") }}</td>
{% endif %}
<td class="bright weather-icon">
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
</td>
<td class="align-right bright max-temp">
{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
<td class="align-right min-temp">
{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}
</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">
{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}
</td>
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">
{{ f.precipitationProbability | unit('precip', '%') }}
</td>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right dimmed uv-index">
{{ f.uv_index }}
<span class="wi dimmed weathericon wi-hot"></span>
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
<td class="bright weather-icon">
<span class="wi weathericon wi-{{ f.weatherType }}"></span>
</td>
<td class="align-right bright max-temp">{{ f.maxTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
<td class="align-right min-temp">{{ f.minTemperature | roundValue | unit("temperature") | decimalSymbol }}</td>
{% if config.showPrecipitationAmount %}
<td class="align-right bright precipitation-amount">{{ f.precipitationAmount | unit("precip", f.precipitationUnits) }}</td>
{% endif %}
{% if config.showPrecipitationProbability %}
<td class="align-right bright precipitation-prob">{{ f.precipitationProbability | unit('precip', '%') }}</td>
{% endif %}
{% if config.showUVIndex %}
<td class="align-right dimmed uv-index">
{{ f.uv_index }}
<span class="wi dimmed weathericon wi-hot"></span>
</td>
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `forecast` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{forecast | dump}}</div> -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ forecast | dump }}</div> -->

View File

@@ -1,52 +1,48 @@
{% if hourly %}
{% 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.showUVIndex %}
<td class="align-right bright uv-index">
{% if hour.uv_index!=0 %}
{{ hour.uv_index }}
<span class="wi weathericon wi-hot"></span>
{% endif %}
</td>
{% endif %}
{% if config.showHumidity != "none" %}
<td class="align-left bright humidity-hourly">
{{ hour.humidity }}
<span class="wi wi-humidity humidity-icon"></span>
</td>
{% endif %}
{% if config.showPrecipitationAmount %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-amount">
{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}
</td>
{% endif %}
{% endif %}
{% if config.showPrecipitationProbability %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-prob">
{{ hour.precipitationProbability | unit('precip', '%') }}
</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% 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.showUVIndex %}
<td class="align-right bright uv-index">
{% if hour.uv_index!=0 %}
{{ hour.uv_index }}
<span class="wi weathericon wi-hot"></span>
{% endif %}
</td>
{% endif %}
{% if config.showHumidity != "none" %}
<td class="align-left bright humidity-hourly">
{{ hour.humidity }}
<span class="wi wi-humidity humidity-icon"></span>
</td>
{% endif %}
{% if config.showPrecipitationAmount %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-amount">{{ hour.precipitationAmount | unit("precip", hour.precipitationUnits) }}</td>
{% endif %}
{% endif %}
{% if config.showPrecipitationProbability %}
{% if (not config.hideZeroes or hour.precipitationAmount>0) %}
<td class="align-right bright precipitation-prob">{{ hour.precipitationProbability | unit('precip', '%') }}</td>
{% endif %}
{% endif %}
</tr>
{% set currentStep = currentStep + 1 %}
{% endfor %}
</table>
{% else %}
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
<div class="dimmed light small">{{ "LOADING" | translate }}</div>
{% endif %}
<!-- Uncomment the line below to see the contents of the `hourly` object. -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{hourly | dump}}</div> -->
<!-- <div style="word-wrap:break-word" class="xsmall dimmed">{{ hourly | dump }}</div> -->

View File

@@ -24,6 +24,8 @@
* with locations you can search under column B (English Names), with the corresponding siteCode under
* column A (Codes) and provCode under column C (Province).
*
* Acknowledgement: Some logic and code for parsing Environment Canada web pages is based on material from MMM-EnvCanada
*
* License to use Environment Canada (EC) data is detailed here:
* https://eccc-msc.github.io/open-data/licence/readme_en/
*/
@@ -49,6 +51,9 @@ WeatherProvider.register("envcanada", {
this.todayTempCacheMax = 0;
this.todayCached = false;
this.cacheCurrentTemp = 999;
this.lastCityPageCurrent = " ";
this.lastCityPageForecast = " ";
this.lastCityPageHourly = " ";
},
/*
@@ -63,69 +68,158 @@ WeatherProvider.register("envcanada", {
* Override the fetchCurrentWeather method to query EC and construct a Current weather object
*/
fetchCurrentWeather () {
this.fetchData(this.getUrl(), "xml")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
this.setCurrentWeather(currentWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada site data ... ", request);
})
.finally(() => this.updateAvailable());
this.fetchCommon("Current");
},
/*
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
* Override the fetchWeatherForecast method to query EC and construct Forecast/Daily weather objects
*/
fetchWeatherForecast () {
this.fetchData(this.getUrl(), "xml")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
this.setWeatherForecast(forecastWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada forecast data ... ", request);
})
.finally(() => this.updateAvailable());
this.fetchCommon("Forecast");
},
/*
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
* Override the fetchWeatherHourly method to query EC and construct Hourly weather objects
*/
fetchWeatherHourly () {
this.fetchData(this.getUrl(), "xml")
.then((data) => {
if (!data) {
// Did not receive usable new data.
return;
}
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
this.setWeatherHourly(hourlyWeather);
})
.catch(function (request) {
Log.error("Could not load EnvCanada hourly data ... ", request);
})
.finally(() => this.updateAvailable());
this.fetchCommon("Hourly");
},
/*
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
* URL defaults to the English version simply because there is no language dependency in the data
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
* Because the process to fetch weather data is virtually the same for Current, Forecast/Daily, and Hourly weather,
* a common module is used to access the EC weather data. The only customization (based on the caller of this routine)
* is how the data will be parsed to satisfy the Weather module config in Config.js
*
* Accessing EC weather data is accomplished in 2 steps:
*
* 1. Query the MSC Datamart Index page, which returns a list of all the filenames for all the cities that have
* weather data currently available.
*
* 2. With the city filename identified, build the appropriate URL and get the weather data (XML document) for the
* city specified in the Weather module Config information
*/
fetchCommon (target) {
const forecastURL = this.getUrl(); // Get the approriate URL for the MSC Datamart Index page
Log.debug(`[weather.envcanada] ${target} Index url: ${forecastURL}`);
this.fetchData(forecastURL, "xml") // Query the Index page URL
.then((indexData) => {
if (!indexData) {
// Did not receive usable new data.
Log.info(`weather.envcanada ${target} - did not receive usable index data`);
this.updateAvailable(); // If there were issues, update anyways to reset timer
return;
}
/**
* With the Index page read, we must locate the filename/link for the specified city (aka Sitecode).
* This is done by building the city filename and searching for it on the Index page. Once found,
* extract the full filename (a unique name that includes dat/time, filename, etc.) and then add it
* to the Index page URL to create the proper URL pointing to the city's weather data. Finally, read the
* URL to pull in the city's XML document so that weather data can be parsed and displayed.
*/
let forecastFile = "";
let forecastFileURL = "";
const fileSuffix = `${this.config.siteCode}_en.xml`; // Build city filename
const nextFile = indexData.body.innerHTML.split(fileSuffix); // Find filename on Index page
if (nextFile.length > 1) { // Parse out the full unqiue file city filename
// Find the last occurrence
forecastFile = nextFile[nextFile.length - 2].slice(-41) + fileSuffix;
forecastFileURL = forecastURL + forecastFile; // Create full URL to the city's weather data
}
Log.debug(`[weather.envcanada] ${target} Citypage url: ${forecastFileURL}`);
/*
* If the Citypage filename has not changed since the last Weather refresh, the forecast has not changed and
* and therefore we can skip reading the Citypage URL.
*/
if (target === "Current" && this.lastCityPageCurrent === forecastFileURL) {
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
this.updateAvailable(); // Update anyways to reset refresh timer
return;
}
if (target === "Forecast" && this.lastCityPageForecast === forecastFileURL) {
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
this.updateAvailable(); // Update anyways to reset refresh timer
return;
}
if (target === "Hourly" && this.lastCityPageHourly === forecastFileURL) {
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
this.updateAvailable(); // Update anyways to reset refresh timer
return;
}
this.fetchData(forecastFileURL, "xml") // Read city's URL to get weather data
.then((cityData) => {
if (!cityData) {
// Did not receive usable new data.
Log.info(`weather.envcanada ${target} - did not receive usable citypage data`);
return;
}
/*
* With the city's weather data read, parse the resulting XML document for the appropriate weather data
* elements to create a weather object. Next, set Weather modules details from that object.
*/
Log.debug(`[weather.envcanada] ${target} - Citypage has been read and will be processed for updates`);
if (target === "Current") {
const currentWeather = this.generateWeatherObjectFromCurrentWeather(cityData);
this.setCurrentWeather(currentWeather);
this.lastCityPageCurrent = forecastFileURL;
}
if (target === "Forecast") {
const forecastWeather = this.generateWeatherObjectsFromForecast(cityData);
this.setWeatherForecast(forecastWeather);
this.lastCityPageForecast = forecastFileURL;
}
if (target === "Hourly") {
const hourlyWeather = this.generateWeatherObjectsFromHourly(cityData);
this.setWeatherHourly(hourlyWeather);
this.lastCityPageHourly = forecastFileURL;
}
})
.catch(function (cityRequest) {
Log.info(`weather.envcanada ${target} - could not load citypage data from: ${forecastFileURL}`);
})
.finally(() => this.updateAvailable()); // Update no matter what to reset weather refresh timer
})
.catch(function (indexRequest) {
Log.error(`weather.envcanada ${target} - could not load index data ... `, indexRequest);
this.updateAvailable(); // If there were issues, update anyways to reset timer
});
},
/*
* Build the EC Index page URL based on current GMT hour. The Index page will provide a list of links for each city
* that will, in turn, provide actual weather data. The URL is comprised of 3 parts:
*
* Fixed value + Prov code specified in Weather module Config.js + current hour as GMT
*/
getUrl () {
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
let forecastURL = `https://dd.weather.gc.ca/citypage_weather/${this.config.provCode}`;
const hour = this.getCurrentHourGMT();
forecastURL += `/${hour}/`;
return forecastURL;
},
/*
* Get current hour-of-day in GMT context
*/
getCurrentHourGMT () {
const now = new Date();
return now.toISOString().substring(11, 13); // "HH" in GMT
},
/*
@@ -151,7 +245,6 @@ WeatherProvider.register("envcanada", {
}
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
@@ -214,7 +307,7 @@ WeatherProvider.register("envcanada", {
/*
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
* 2 elements. the first element for a day details the Today (daytime) forecast while the second
* element details the Tonight (nightime) forecast. Element 0 is always for the current day.
* element details the Tonight (nighttime) forecast. Element 0 is always for the current day.
*
* However... the forecast is somewhat 'rolling'.
*
@@ -225,7 +318,7 @@ WeatherProvider.register("envcanada", {
*
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
* off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Element 11 will contain a forecast for a 6th day,
* but only for the Today portion (not Tonight). This module will create a 6-day forecast using
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
*
@@ -436,17 +529,17 @@ WeatherProvider.register("envcanada", {
* then it will be displayed ONLY if no POP is present.
*
* POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
* people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
* people are more interested in seeing. While EC provides a separate POP for daytime and nighttime portions
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
* the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
* the nighttime forecast after a certain point in the afternoon. As such, we will be showing the nighttime POP
* (if one exists) in that specific scenario.
*
* Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
* people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
* people are interested in seeing. While EC provides a separate accumulation for daytime and nighttime portions
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
* the nightime forecast after a certain point in that specific scenario.
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
* the nighttime forecast after a certain point in that specific scenario.
*/
setPrecipitation (weather, foreGroup, today) {
if (foreGroup[today].querySelector("precipitation accumulation")) {

View File

@@ -13,7 +13,7 @@ WeatherProvider.register("openmeteo", {
/*
* Set the name of the provider.
* Not strictly required, but helps for debugging.
* Not strictly required but helps for debugging.
*/
providerName: "Open-Meteo",
@@ -348,7 +348,7 @@ WeatherProvider.register("openmeteo", {
generateWeatherDayFromCurrentWeather (weather) {
/**
* Since some units comes from API response "splitted" into daily, hourly and current_weather
* Since some units come from API response "splitted" into daily, hourly and current_weather
* every time you request it, you have to ensure to get the data from the right place every time.
* For the current weather case, the response have the following structure (after transposing):
* ```
@@ -381,6 +381,7 @@ WeatherProvider.register("openmeteo", {
currentWeather.maxTemperature = parseFloat(weather.daily[0].temperature_2m_max);
currentWeather.weatherType = this.convertWeatherType(weather.current_weather.weathercode, currentWeather.isDayTime());
currentWeather.humidity = parseFloat(weather.hourly[h].relativehumidity_2m);
currentWeather.feelsLikeTemp = parseFloat(weather.hourly[h].apparent_temperature);
currentWeather.rain = parseFloat(weather.hourly[h].rain);
currentWeather.snow = parseFloat(weather.hourly[h].snowfall * 10);
currentWeather.precipitationAmount = parseFloat(weather.hourly[h].precipitation);

View File

@@ -254,7 +254,7 @@ WeatherProvider.register("smhi", {
* Helper method to get a property from the returned data set.
* @param {object} currentWeatherData Weatherdata to get from
* @param {string} name The name of the property
* @returns {*} The value of the property in the weatherdata
* @returns {string} The value of the property in the weatherdata
*/
paramValue (currentWeatherData, name) {
return currentWeatherData.parameters.filter((p) => p.name === name).flatMap((p) => p.values)[0];

View File

@@ -218,7 +218,7 @@ WeatherProvider.register("weathergov", {
currentWeather.minTemperature = currentWeatherData.minTemperatureLast24Hours.value;
currentWeather.maxTemperature = currentWeatherData.maxTemperatureLast24Hours.value;
currentWeather.humidity = Math.round(currentWeatherData.relativeHumidity.value);
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour.value ? currentWeatherData.precipitationLastHour.value : currentWeatherData.precipitationLast3Hours.value;
currentWeather.precipitationAmount = currentWeatherData.precipitationLastHour?.value ?? currentWeatherData.precipitationLast3Hours?.value;
if (currentWeatherData.heatIndex.value !== null) {
currentWeather.feelsLikeTemp = currentWeatherData.heatIndex.value;
} else if (currentWeatherData.windChill.value !== null) {

View File

@@ -1,9 +1,6 @@
.weather .weathericon,
.weather .fa-home {
font-size: 75%;
line-height: 65px;
display: inline-block;
transform: translate(0, -3px);
}
.weather .humidity-icon {
@@ -37,10 +34,6 @@
padding-right: 0;
}
.weather tr .weathericon {
line-height: 25px;
}
.weather tr.colored .min-temp {
color: #bcddff;
}
@@ -48,3 +41,9 @@
.weather tr.colored .max-temp {
color: #ff8e99;
}
.weather .type-temp {
display: flex;
align-items: baseline;
gap: 10px;
}

View File

@@ -163,11 +163,12 @@ Module.register("weather", {
// What to do when the weather provider has new information available?
updateAvailable () {
Log.log("New weather information available.");
this.updateDom(0);
// this value was changed from 0 to 300 to stabilize weather tests:
this.updateDom(300);
this.scheduleUpdate();
if (this.weatherProvider.currentWeather()) {
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType?.replace("-", "_") });
}
const notificationPayload = {

View File

@@ -53,7 +53,7 @@ const WeatherUtils = {
/**
* Convert temp (from degrees C) into imperial or metric unit depending on
* your config
* @param {number} tempInC the temperature in celsius you want to convert
* @param {number} tempInC the temperature in Celsius you want to convert
* @param {string} unit can be 'imperial' or 'metric'
* @returns {number} the converted temperature
*/
@@ -61,6 +61,15 @@ const WeatherUtils = {
return unit === "imperial" ? tempInC * 1.8 + 32 : tempInC;
},
/**
* Convert temp (from degrees C) into metric unit
* @param {number} tempInF the temperature in Fahrenheit you want to convert
* @returns {number} the converted temperature
*/
convertTempToMetric (tempInF) {
return ((tempInF - 32) * 5) / 9;
},
/**
* Convert wind speed into another unit.
* @param {number} windInMS the windspeed in meter/sec you want to convert
@@ -118,27 +127,51 @@ const WeatherUtils = {
return kmh * 0.27777777777778;
},
/**
* Taken from https://community.home-assistant.io/t/calculating-apparent-feels-like-temperature/370834/18
* @param {number} temperature temperature in degrees Celsius
* @param {number} windSpeed wind speed in meter/second
* @param {number} humidity relative humidity in percent
* @returns {number} the feels like temperature in degrees Celsius
*/
calculateFeelsLike (temperature, windSpeed, humidity) {
const windInMph = this.convertWind(windSpeed, "imperial");
const tempInF = this.convertTemp(temperature, "imperial");
let feelsLike = tempInF;
if (windInMph > 3 && tempInF < 50) {
feelsLike = Math.round(35.74 + 0.6215 * tempInF - 35.75 * Math.pow(windInMph, 0.16) + 0.4275 * tempInF * Math.pow(windInMph, 0.16));
} else if (tempInF > 80 && humidity > 40) {
feelsLike
= -42.379
+ 2.04901523 * tempInF
+ 10.14333127 * humidity
- 0.22475541 * tempInF * humidity
- 6.83783 * Math.pow(10, -3) * tempInF * tempInF
- 5.481717 * Math.pow(10, -2) * humidity * humidity
+ 1.22874 * Math.pow(10, -3) * tempInF * tempInF * humidity
+ 8.5282 * Math.pow(10, -4) * tempInF * humidity * humidity
- 1.99 * Math.pow(10, -6) * tempInF * tempInF * humidity * humidity;
let HI;
let WC = tempInF;
// Calculate wind chill for certain conditions
if (tempInF <= 70 && windInMph >= 3) {
WC = 35.74 + (0.6215 * tempInF) - 35.75 * Math.pow(windInMph, 0.16) + ((0.4275 * tempInF) * Math.pow(windInMph, 0.16));
}
return ((feelsLike - 32) * 5) / 9;
// Steadman Heat Index Vorberechnung
const STEADMAN_HI = 0.5 * (tempInF + 61.0 + ((tempInF - 68.0) * 1.2) + (humidity * 0.094));
if (STEADMAN_HI >= 80) {
// Rothfusz-Komplex
const ROTHFUSZ_HI = -42.379 + 2.04901523 * tempInF + 10.14333127 * humidity - 0.22475541 * tempInF * humidity - 0.00683783 * tempInF * tempInF - 0.05481717 * humidity * humidity + 0.00122874 * tempInF * tempInF * humidity + 0.00085282 * tempInF * humidity * humidity - 0.00000199 * tempInF * tempInF * humidity * humidity;
HI = ROTHFUSZ_HI;
if (humidity < 13 && tempInF > 80 && tempInF < 112) {
const ADJUSTMENT = ((13 - humidity) / 4) * Math.pow(Math.abs(17 - (tempInF - 95)), 0.5) / 17; // sqrt Teil
HI = HI - ADJUSTMENT;
} else if (humidity > 85 && tempInF > 80 && tempInF < 87) {
const ADJUSTMENT = ((humidity - 85) / 10) * ((87 - tempInF) / 5);
HI = HI + ADJUSTMENT;
}
} else { HI = STEADMAN_HI; }
// Feuchte Lastberechnung FL
let FL;
if (tempInF < 50) { FL = WC; }
else if (tempInF >= 50 && tempInF < 70) { FL = ((70 - tempInF) / 20) * WC + ((tempInF - 50) / 20) * HI; }
else if (tempInF >= 70) { FL = HI; }
return this.convertTempToMetric(FL);
},
/**