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