Skip to content

Commit e886821

Browse files
CrazylegstooKristjanESPERANTOveeck
authored
Fix for envcanada Provider to use new Environment Canada weather data access (#3878)
Earlier in 2025, Environment Canada changed the process to access weather data for Canadian cities. This change was raised in Issue #3822 as a Bug, which is addressed in this Provider update. There are no Magic Mirror UI changes from this update. The 'old' method to access Environment Canada involved accessing a static URL based on a City identifier which would result in an XML document containing the appropriate weather data. The 'new' method is a 2 step process. The first step is to access a time-sensitive URL that contains a list of links to various cities that have weather data available. The second step requires finding the correct city in that list based on a City identifier, and then accessing that unique URL to access an XML document containing the appropriate weather data. The changes made to the envcanada Provider code are solely aimed at using the new 2-step method to access a specified City's weather data. Since the resulting XML document structure has not changed, no other code in envcanada required changes. Note that a ChangeLog entry is included in this PR. --------- Co-authored-by: Kristjan ESPERANTO <[email protected]> Co-authored-by: veeck <[email protected]>
1 parent a49fbed commit e886821

File tree

2 files changed

+145
-51
lines changed

2 files changed

+145
-51
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Thanks to: @dathbe.
5959
- [core] Fixed socket.io timeout when server is slow to send notification, notification lost at client (#3380)
6060
- [tests] refactor AnimateCSS tests after jsdom 27 upgrade (#3891)
6161
- [weather] Use `apparent_temperature` data from openmeteo's hourly weather for current feelsLikeTemp (#3868).
62+
- [weather] Updated envcanada Provider to use new database/URL schema for accessing weather data (#3822).
6263

6364
## [2.32.0] - 2025-07-01
6465

modules/default/weather/providers/envcanada.js

Lines changed: 144 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
* with locations you can search under column B (English Names), with the corresponding siteCode under
2525
* column A (Codes) and provCode under column C (Province).
2626
*
27+
* Acknowledgement: Some logic and code for parsing Environment Canada web pages is based on material from MMM-EnvCanada
28+
*
2729
* License to use Environment Canada (EC) data is detailed here:
2830
* https://eccc-msc.github.io/open-data/licence/readme_en/
2931
*/
@@ -49,6 +51,9 @@ WeatherProvider.register("envcanada", {
4951
this.todayTempCacheMax = 0;
5052
this.todayCached = false;
5153
this.cacheCurrentTemp = 999;
54+
this.lastCityPageCurrent = " ";
55+
this.lastCityPageForecast = " ";
56+
this.lastCityPageHourly = " ";
5257
},
5358

5459
/*
@@ -63,69 +68,158 @@ WeatherProvider.register("envcanada", {
6368
* Override the fetchCurrentWeather method to query EC and construct a Current weather object
6469
*/
6570
fetchCurrentWeather () {
66-
this.fetchData(this.getUrl(), "xml")
67-
.then((data) => {
68-
if (!data) {
69-
// Did not receive usable new data.
70-
return;
71-
}
72-
const currentWeather = this.generateWeatherObjectFromCurrentWeather(data);
73-
74-
this.setCurrentWeather(currentWeather);
75-
})
76-
.catch(function (request) {
77-
Log.error("Could not load EnvCanada site data ... ", request);
78-
})
79-
.finally(() => this.updateAvailable());
71+
this.fetchCommon("Current");
8072
},
8173

8274
/*
83-
* Override the fetchWeatherForecast method to query EC and construct Forecast weather objects
75+
* Override the fetchWeatherForecast method to query EC and construct Forecast/Daily weather objects
8476
*/
8577
fetchWeatherForecast () {
86-
this.fetchData(this.getUrl(), "xml")
87-
.then((data) => {
88-
if (!data) {
89-
// Did not receive usable new data.
90-
return;
91-
}
92-
const forecastWeather = this.generateWeatherObjectsFromForecast(data);
9378

94-
this.setWeatherForecast(forecastWeather);
95-
})
96-
.catch(function (request) {
97-
Log.error("Could not load EnvCanada forecast data ... ", request);
98-
})
99-
.finally(() => this.updateAvailable());
79+
this.fetchCommon("Forecast");
80+
10081
},
10182

10283
/*
103-
* Override the fetchWeatherHourly method to query EC and construct Forecast weather objects
84+
* Override the fetchWeatherHourly method to query EC and construct Hourly weather objects
10485
*/
10586
fetchWeatherHourly () {
106-
this.fetchData(this.getUrl(), "xml")
107-
.then((data) => {
108-
if (!data) {
87+
this.fetchCommon("Hourly");
88+
},
89+
90+
/*
91+
* Because the process to fetch weather data is virtually the same for Current, Forecast/Daily, and Hourly weather,
92+
* a common module is used to access the EC weather data. The only customization (based on the caller of this routine)
93+
* is how the data will be parsed to satisfy the Weather module config in Config.js
94+
*
95+
* Accessing EC weather data is accomplished in 2 steps:
96+
*
97+
* 1. Query the MSC Datamart Index page, which returns a list of all the filenames for all the cities that have
98+
* weather data currently available.
99+
*
100+
* 2. With the city filename identified, build the appropriate URL and get the weather data (XML document) for the
101+
* city specified in the Weather module Config information
102+
*/
103+
fetchCommon (target) {
104+
const forecastURL = this.getUrl(); // Get the approriate URL for the MSC Datamart Index page
105+
106+
Log.debug(`[weather.envcanada] ${target} Index url: ${forecastURL}`);
107+
108+
this.fetchData(forecastURL, "xml") // Query the Index page URL
109+
.then((indexData) => {
110+
if (!indexData) {
109111
// Did not receive usable new data.
112+
Log.info(`weather.envcanada ${target} - did not receive usable index data`);
113+
this.updateAvailable(); // If there were issues, update anyways to reset timer
110114
return;
111115
}
112-
const hourlyWeather = this.generateWeatherObjectsFromHourly(data);
113116

114-
this.setWeatherHourly(hourlyWeather);
115-
})
116-
.catch(function (request) {
117-
Log.error("Could not load EnvCanada hourly data ... ", request);
117+
/**
118+
* With the Index page read, we must locate the filename/link for the specified city (aka Sitecode).
119+
* This is done by building the city filename and searching for it on the Index page. Once found,
120+
* extract the full filename (a unique name that includes dat/time, filename, etc.) and then add it
121+
* to the Index page URL to create the proper URL pointing to the city's weather data. Finally, read the
122+
* URL to pull in the city's XML document so that weather data can be parsed and displayed.
123+
*/
124+
125+
let forecastFile = "";
126+
let forecastFileURL = "";
127+
const fileSuffix = `${this.config.siteCode}_en.xml`; // Build city filename
128+
const nextFile = indexData.body.innerHTML.split(fileSuffix); // Find filename on Index page
129+
130+
if (nextFile.length > 1) { // Parse out the full unqiue file city filename
131+
// Find the last occurrence
132+
forecastFile = nextFile[nextFile.length - 2].slice(-41) + fileSuffix;
133+
forecastFileURL = forecastURL + forecastFile; // Create full URL to the city's weather data
134+
}
135+
136+
Log.debug(`[weather.envcanada] ${target} Citypage url: ${forecastFileURL}`);
137+
138+
/*
139+
* If the Citypage filename has not changed since the last Weather refresh, the forecast has not changed and
140+
* and therefore we can skip reading the Citypage URL.
141+
*/
142+
143+
if (target === "Current" && this.lastCityPageCurrent === forecastFileURL) {
144+
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
145+
this.updateAvailable(); // Update anyways to reset refresh timer
146+
return;
147+
}
148+
149+
if (target === "Forecast" && this.lastCityPageForecast === forecastFileURL) {
150+
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
151+
this.updateAvailable(); // Update anyways to reset refresh timer
152+
return;
153+
}
154+
155+
if (target === "Hourly" && this.lastCityPageHourly === forecastFileURL) {
156+
Log.debug(`[weather.envcanada] ${target} - Newest Citypage has already been seen - skipping!`);
157+
this.updateAvailable(); // Update anyways to reset refresh timer
158+
return;
159+
}
160+
161+
this.fetchData(forecastFileURL, "xml") // Read city's URL to get weather data
162+
.then((cityData) => {
163+
if (!cityData) {
164+
// Did not receive usable new data.
165+
Log.info(`weather.envcanada ${target} - did not receive usable citypage data`);
166+
return;
167+
}
168+
169+
/*
170+
* With the city's weather data read, parse the resulting XML document for the appropriate weather data
171+
* elements to create a weather object. Next, set Weather modules details from that object.
172+
*/
173+
Log.debug(`[weather.envcanada] ${target} - Citypage has been read and will be processed for updates`);
174+
175+
if (target === "Current") {
176+
const currentWeather = this.generateWeatherObjectFromCurrentWeather(cityData);
177+
this.setCurrentWeather(currentWeather);
178+
this.lastCityPageCurrent = forecastFileURL;
179+
}
180+
181+
if (target === "Forecast") {
182+
const forecastWeather = this.generateWeatherObjectsFromForecast(cityData);
183+
this.setWeatherForecast(forecastWeather);
184+
this.lastCityPageForecast = forecastFileURL;
185+
}
186+
187+
if (target === "Hourly") {
188+
const hourlyWeather = this.generateWeatherObjectsFromHourly(cityData);
189+
this.setWeatherHourly(hourlyWeather);
190+
this.lastCityPageHourly = forecastFileURL;
191+
}
192+
})
193+
.catch(function (cityRequest) {
194+
Log.info(`weather.envcanada ${target} - could not load citypage data from: ${forecastFileURL}`);
195+
})
196+
.finally(() => this.updateAvailable()); // Update no matter what to reset weather refresh timer
118197
})
119-
.finally(() => this.updateAvailable());
198+
.catch(function (indexRequest) {
199+
Log.error(`weather.envcanada ${target} - could not load index data ... `, indexRequest);
200+
this.updateAvailable(); // If there were issues, update anyways to reset timer
201+
});
120202
},
121203

122204
/*
123-
* Build the EC URL based on the Site Code and Province Code specified in the config params. Note that the
124-
* URL defaults to the English version simply because there is no language dependency in the data
125-
* being accessed. This is only pertinent when using the EC data elements that contain a textual forecast.
205+
* Build the EC Index page URL based on current GMT hour. The Index page will provide a list of links for each city
206+
* that will, in turn, provide actual weather data. The URL is comprised of 3 parts:
207+
*
208+
* Fixed value + Prov code specified in Weather module Config.js + current hour as GMT
126209
*/
127210
getUrl () {
128-
return `https://dd.weather.gc.ca/citypage_weather/xml/${this.config.provCode}/${this.config.siteCode}_e.xml`;
211+
let forecastURL = `https://dd.weather.gc.ca/citypage_weather/${this.config.provCode}`;
212+
const hour = this.getCurrentHourGMT();
213+
forecastURL += `/${hour}/`;
214+
return forecastURL;
215+
},
216+
217+
/*
218+
* Get current hour-of-day in GMT context
219+
*/
220+
getCurrentHourGMT () {
221+
const now = new Date();
222+
return now.toISOString().substring(11, 13); // "HH" in GMT
129223
},
130224

131225
/*
@@ -151,7 +245,6 @@ WeatherProvider.register("envcanada", {
151245
}
152246

153247
currentWeather.windSpeed = WeatherUtils.convertWindToMs(ECdoc.querySelector("siteData currentConditions wind speed").textContent);
154-
155248
currentWeather.windFromDirection = ECdoc.querySelector("siteData currentConditions wind bearing").textContent;
156249

157250
currentWeather.humidity = ECdoc.querySelector("siteData currentConditions relativeHumidity").textContent;
@@ -214,7 +307,7 @@ WeatherProvider.register("envcanada", {
214307
/*
215308
* The EC forecast is held in a 12-element array - Elements 0 to 11 - with each day encompassing
216309
* 2 elements. the first element for a day details the Today (daytime) forecast while the second
217-
* element details the Tonight (nightime) forecast. Element 0 is always for the current day.
310+
* element details the Tonight (nighttime) forecast. Element 0 is always for the current day.
218311
*
219312
* However... the forecast is somewhat 'rolling'.
220313
*
@@ -225,7 +318,7 @@ WeatherProvider.register("envcanada", {
225318
*
226319
* But, if the EC forecast is queried in late afternoon, the Current Today forecast will be rolled
227320
* off and Element 0 will contain Current Tonight. From there, the next 5 days will be contained in
228-
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Elelement 11 will contain a forecast for a 6th day,
321+
* Elements 1/2, 3/4, 5/6, 7/8, and 9/10. As well, Element 11 will contain a forecast for a 6th day,
229322
* but only for the Today portion (not Tonight). This module will create a 6-day forecast using
230323
* Elements 0 to 11, and will ignore the additional Todat forecast in Element 11.
231324
*
@@ -436,17 +529,17 @@ WeatherProvider.register("envcanada", {
436529
* then it will be displayed ONLY if no POP is present.
437530
*
438531
* POP Logic: By default, we want to show the POP for 'daytime' since we are presuming that is what
439-
* people are more interested in seeing. While EC provides a separate POP for daytime and nightime portions
532+
* people are more interested in seeing. While EC provides a separate POP for daytime and nighttime portions
440533
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
441-
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
442-
* the nightime forecast after a certain point in the afternoon. As such, we will be showing the nightime POP
534+
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
535+
* the nighttime forecast after a certain point in the afternoon. As such, we will be showing the nighttime POP
443536
* (if one exists) in that specific scenario.
444537
*
445538
* Accumulation Logic: Similar to POP, we want to show accumulation for 'daytime' since we presume that is what
446-
* people are interested in seeing. While EC provides a separate accumulation for daytime and nightime portions
539+
* people are interested in seeing. While EC provides a separate accumulation for daytime and nighttime portions
447540
* of each day, the weather module does not really allow for that view of a daily forecast. There we will
448-
* ignore any nightime portion. There is an exception however! For the Current day, the EC data will only show
449-
* the nightime forecast after a certain point in that specific scenario.
541+
* ignore any nighttime portion. There is an exception however! For the Current day, the EC data will only show
542+
* the nighttime forecast after a certain point in that specific scenario.
450543
*/
451544
setPrecipitation (weather, foreGroup, today) {
452545
if (foreGroup[today].querySelector("precipitation accumulation")) {

0 commit comments

Comments
 (0)