@@ -12,8 +12,8 @@ globals:
1212 - id : nowcast_delay_mins
1313 type : int
1414 restore_value : false
15- # NowCast is calculated over a 12 hour period
16- initial_value : ' 720 '
15+ # NowCast is valid if we have data for 2 out of the 3 most recent hours
16+ initial_value : ' 120 '
1717 - id : pm_2_5_hourly_avg
1818 type : std::vector<float>
1919 restore_value : false
@@ -81,7 +81,7 @@ sensor:
8181 lambda : |
8282 // Insert the current value
8383 float current = id(pm_2_5_1h_avg).state;
84- if (!isnan(current)) {
84+ if (!isnan(current) && current > 0 ) {
8585 id(pm_2_5_hourly_avg).insert(id(pm_2_5_hourly_avg).begin(), current);
8686 // Truncate anything past the first 24
8787 if (id(pm_2_5_hourly_avg).size() > 24) {
@@ -107,7 +107,7 @@ sensor:
107107 lambda : |
108108 // Insert the current value
109109 float current = id(pm_10_0_1h_avg).state;
110- if (!isnan(current)) {
110+ if (!isnan(current) && current > 0 ) {
111111 id(pm_10_0_hourly_avg).insert(id(pm_10_0_hourly_avg).begin(), current);
112112 // Truncate anything past the first 24
113113 if (id(pm_10_0_hourly_avg).size() > 24) {
@@ -123,51 +123,63 @@ script:
123123 int aqi_2_5 = -1;
124124 int aqi_10_0 = -1;
125125
126- // AQI is calculated over a 24 hour minimum, but EPA says it's acceptable to
126+ // AQI is calculated over a 24 hour minimum but EPA says it's acceptable to
127127 // report at 75%, or 18 hours: https://forum.airnowtech.org/t/aqi-calculations-overview-ozone-pm2-5-and-pm10/168
128- int size_2_5 = id(pm_2_5_hourly_avg).size();
129- if (size_2_5 > 24) {
130- size_2_5 = 24;
128+ int size_2_5 = 0;
129+ float sum_2_5 = 0.0;
130+ int hourly_2_5_size = std::min(24, static_cast<int>(id(pm_2_5_hourly_avg).size()));
131+ for (int i = 0; i < hourly_2_5_size; i++) {
132+ float pm = id(pm_2_5_hourly_avg)[i];
133+ if (!isnan(pm) && pm > 0) {
134+ sum_2_5 += pm;
135+ size_2_5++;
136+ }
131137 }
138+
132139 if (size_2_5 >= 18) {
133140
134- float sum = 0.0;
135- for (int i = 0; i < size_2_5; i++) {
136- sum += id(pm_2_5_hourly_avg)[i];
137- }
141+ float pm25 = sum_2_5 / (float)size_2_5;
142+ pm25 = static_cast<int>(pm25 * 10) / 10.0; // pm25 is truncated to the nearest 0.1
138143
139- float pm25 = sum / (float)size_2_5;
140- if (pm25 < 12.0) {
141- aqi_2_5 = (50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0;
144+ // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
145+ // 2024 EPA breakpoints: https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
146+ if (pm25 < 9.0) {
147+ aqi_2_5 = (50.0 - 0.0) / (9.0 - 0.0) * (pm25 - 0.0) + 0.0;
142148 } else if (pm25 < 35.4) {
143- aqi_2_5 = (100.0 - 51.0) / (35.4 - 12 .1) * (pm25 - 12 .1) + 51.0;
149+ aqi_2_5 = (100.0 - 51.0) / (35.4 - 9 .1) * (pm25 - 9 .1) + 51.0;
144150 } else if (pm25 < 55.4) {
145151 aqi_2_5 = (150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0;
146- } else if (pm25 < 150.4) {
147- aqi_2_5 = (200.0 - 151.0) / (150.4 - 55.5) * (pm25 - 55.5) + 151.0;
148- } else if (pm25 < 250.4) {
149- aqi_2_5 = (300.0 - 201.0) / (250.4 - 150.5) * (pm25 - 150.5) + 201.0;
150- } else if (pm25 < 350.4) {
151- aqi_2_5 = (400.0 - 301.0) / (350.4 - 250.5) * (pm25 - 250.5) + 301.0;
152- } else if (pm25 < 500.4) {
153- aqi_2_5 = (500.0 - 401.0) / (500.4 - 350.5) * (pm25 - 350.5) + 401.0;
152+ } else if (pm25 < 125.4) {
153+ aqi_2_5 = (200.0 - 151.0) / (125.4 - 55.5) * (pm25 - 55.5) + 151.0;
154+ } else if (pm25 < 225.4) {
155+ aqi_2_5 = (300.0 - 201.0) / (225.4 - 125.5) * (pm25 - 125.5) + 201.0;
156+ } else if (pm25 < 500.0) {
157+ aqi_2_5 = (500.0 - 301.0) / (500.0 - 225.5) * (pm25 - 225.5) + 301.0;
154158 } else {
155- aqi_2_5 = 500; // everything higher is just counted as 500
159+ // everything higher is just counted as 500
160+ aqi_2_5 = 500;
156161 }
162+
157163 }
158164
159- int size_10_0 = id(pm_10_0_hourly_avg).size();
160- if (size_10_0 > 24) {
161- size_10_0 = 24;
165+ int size_10_0 = 0;
166+ float sum_10_0 = 0.0;
167+ int loop_max_10 = std::min(24, static_cast<int>(id(pm_10_0_hourly_avg).size()));
168+ for (int i = 0; i < loop_max_10; i++) {
169+ float pm = id(pm_10_0_hourly_avg)[i];
170+ if (!isnan(pm) && pm > 0) {
171+ sum_10_0 += pm;
172+ size_10_0++;
173+ }
162174 }
175+
163176 if (size_10_0 >= 18) {
164177
165- float sum = 0.0;
166- for (int i = 0; i < size_10_0; i++) {
167- sum += id(pm_10_0_hourly_avg)[i];
168- }
178+ float pm10 = sum_10_0 / (float)size_10_0;
179+ pm10 = static_cast<int>(pm10); // pm10 is truncated to the nearest whole number
169180
170- float pm10 = sum / (float)size_10_0;
181+ // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
182+ // 2024 EPA breakpoints: https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
171183 if (pm10 < 54.0) {
172184 aqi_10_0 = (50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0;
173185 } else if (pm10 < 154.0) {
@@ -178,21 +190,19 @@ script:
178190 aqi_10_0 = (200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0;
179191 } else if (pm10 < 424.0) {
180192 aqi_10_0 = (300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0;
181- } else if (pm10 < 504.0) {
182- aqi_10_0 = (400.0 - 301.0) / (504.0 - 425.0) * (pm10 - 425.0) + 301.0;
183- } else if (pm10 < 604) {
184- aqi_10_0 = (500.0 - 401.0) / (604.0 - 505.0) * (pm10 - 505.0) + 401.0;
193+ } else if (pm10 < 500.0) {
194+ aqi_10_0 = (500.0 - 301.0) / (500.0 - 425.0) * (pm10 - 425.0) + 301.0;
185195 } else {
186- aqi_10_0 = 500; // everything higher is just counted as 500
196+ // everything higher is just counted as 500
197+ aqi_10_0 = 500.0;
187198 }
188199 }
189200
190201 int aqi_calc = std::max(aqi_2_5, aqi_10_0);
191202 if (aqi_calc > 0) {
192203 id(aqi).publish_state(aqi_calc);
193204 // Just in case we're counting down, make sure we set to zero
194- id(aqi_delay_mins) = 0;
195- if (id(aqi_delay_mins) > 0) {
205+ if (isnan(id(aqi_delay_mins)) || id(aqi_delay_mins) > 0) {
196206 id(aqi_delay_mins) = 0;
197207 id(aqi_mins_remaining).publish_state(0);
198208 }
@@ -204,122 +214,147 @@ script:
204214 - lambda : |
205215 int nowcast_2_5 = -1;
206216 int nowcast_10_0 = -1;
217+
218+ int hourly_2_5_size = std::min(12, static_cast<int>(id(pm_2_5_hourly_avg).size()));
219+ int hourly_10_0_size = std::min(12, static_cast<int>(id(pm_10_0_hourly_avg).size()));
207220
208- if (id(pm_2_5_hourly_avg).size() >= 12) {
221+ // nowcast is valid if we have 2 out of the last 3 hours worth of data
222+ if (hourly_2_5_size >= 2) {
209223 // Calculate min and max
210224 float max = 0.0;
211225 float min = 31337.0; // just a random large number
212- for (int i = 0; i < 12 ; i++) {
226+ for (int i = 0; i < hourly_2_5_size ; i++) {
213227 float pm = id(pm_2_5_hourly_avg)[i];
214- if (pm < min) {
215- min = pm;
228+ if (!isnan(pm) && pm > 0) {
229+ if (pm < min) {
230+ min = pm;
231+ }
232+ if (pm > max) {
233+ max = pm;
234+ }
216235 }
217- if (pm > max) {
218- max = pm;
219- }
220- }
221- // Calculate the weight factor
222- float range = max - min;
223- float rate = range / max;
224- float weight_factor = 1.0 - rate;
225- if (weight_factor < 0.5) {
226- weight_factor = 0.5;
227- } else if (weight_factor > 1.0) {
228- weight_factor = 1.0;
229236 }
237+ if (max >= min) {
230238
231- float pm_sum = 0.0;
232- float weight_sum = 0.0;
233- for (int i = 0; i < 12; i++) {
234- float weight_pow = pow(weight_factor, i);
235- pm_sum += id(pm_2_5_hourly_avg)[i] * weight_pow;
236- weight_sum += weight_pow;
237- }
238- float pm25 = pm_sum / weight_sum;
239+ // Calculate the weight factor
240+ float range = max - min;
241+ float rate = range / max;
242+ float weight_factor = 1.0 - rate;
243+ if (weight_factor < 0.5) {
244+ weight_factor = 0.5;
245+ } else if (weight_factor > 1.0) {
246+ weight_factor = 1.0;
247+ }
239248
240- // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
241- if (pm25 < 12.0) {
242- nowcast_2_5 = (50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0;
243- } else if (pm25 < 35.4) {
244- nowcast_2_5 = (100.0 - 51.0) / (35.4 - 12.1) * (pm25 - 12.1) + 51.0;
245- } else if (pm25 < 55.4) {
246- nowcast_2_5 = (150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0;
247- } else if (pm25 < 150.4) {
248- nowcast_2_5 = (200.0 - 151.0) / (150.4 - 55.5) * (pm25 - 55.5) + 151.0;
249- } else if (pm25 < 250.4) {
250- nowcast_2_5 = (300.0 - 201.0) / (250.4 - 150.5) * (pm25 - 150.5) + 201.0;
251- } else if (pm25 < 350.4) {
252- nowcast_2_5 = (400.0 - 301.0) / (350.4 - 250.5) * (pm25 - 250.5) + 301.0;
253- } else if (pm25 < 500.4) {
254- nowcast_2_5 = (500.0 - 401.0) / (500.4 - 350.5) * (pm25 - 350.5) + 401.0;
255- } else {
256- // everything higher is just counted as 500
257- nowcast_2_5 = 500;
249+ float pm_sum = 0.0;
250+ float weight_sum = 0.0;
251+ for (int i = 0; i < hourly_2_5_size; i++) {
252+ float pm = id(pm_2_5_hourly_avg)[i];
253+ if (!isnan(pm) && pm > 0) {
254+ float weight_pow = pow(weight_factor, i);
255+ pm_sum += pm * weight_pow;
256+ weight_sum += weight_pow;
257+ }
258+ }
259+ float pm25 = pm_sum / weight_sum;
260+ pm25 = static_cast<int>(pm25 * 10) / 10.0; // pm25 is truncated to the nearest 0.1
261+
262+ // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
263+ // 2024 EPA breakpoints: https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
264+ if (pm25 < 9.0) {
265+ nowcast_2_5 = (50.0 - 0.0) / (9.0 - 0.0) * (pm25 - 0.0) + 0.0;
266+ } else if (pm25 < 35.4) {
267+ nowcast_2_5 = (100.0 - 51.0) / (35.4 - 9.1) * (pm25 - 9.1) + 51.0;
268+ } else if (pm25 < 55.4) {
269+ nowcast_2_5 = (150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0;
270+ } else if (pm25 < 125.4) {
271+ nowcast_2_5 = (200.0 - 151.0) / (125.4 - 55.5) * (pm25 - 55.5) + 151.0;
272+ } else if (pm25 < 225.4) {
273+ nowcast_2_5 = (300.0 - 201.0) / (225.4 - 125.5) * (pm25 - 125.5) + 201.0;
274+ } else if (pm25 < 500.0) {
275+ nowcast_2_5 = (500.0 - 301.0) / (500.0 - 225.5) * (pm25 - 225.5) + 301.0;
276+ } else {
277+ // everything higher is just counted as 500
278+ nowcast_2_5 = 500;
279+ }
280+
258281 }
259282 }
260283
261- if (id(pm_10_0_hourly_avg).size() >= 12 ) {
284+ if (hourly_10_0_size >= 2 ) {
262285 // Calculate min and max
263286 float max = 0.0;
264287 float min = 31337.0; // just a random large number
265- for (int i = 0; i < 12 ; i++) {
288+ for (int i = 0; i < hourly_10_0_size ; i++) {
266289 float pm = id(pm_10_0_hourly_avg)[i];
267- if (pm < min) {
268- min = pm;
269- }
270- if (pm > max) {
271- max = pm;
290+ if (!isnan(pm) && pm > 0) {
291+ if (pm < min) {
292+ min = pm;
293+ }
294+ if (pm > max) {
295+ max = pm;
296+ }
272297 }
273298 }
274- // Calculate the weight factor
275- float range = max - min;
276- float rate = range / max;
277- float weight_factor = 1.0 - rate;
278- if (weight_factor < 0.5) {
279- weight_factor = 0.5;
280- } else if (weight_factor > 1.0) {
281- weight_factor = 1.0;
282- }
283299
284- float pm_sum = 0.0;
285- float weight_sum = 0.0;
286- for (int i = 0; i < 12; i++) {
287- float weight_pow = pow(weight_factor, i);
288- pm_sum += id(pm_10_0_hourly_avg)[i] * weight_pow;
289- weight_sum += weight_pow;
290- }
291- float pm10 = pm_sum / weight_sum;
300+ if (max >= min) {
292301
293- // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
294- if (pm10 < 54.0) {
295- nowcast_10_0 = (50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0;
296- } else if (pm10 < 154.0) {
297- nowcast_10_0 = (100.0 - 51.0) / (154.0 - 55.0) * (pm10 - 55.0) + 51.0;
298- } else if (pm10 < 254.0) {
299- nowcast_10_0 = (150.0 - 101.0) / (254.0 - 155.0) * (pm10 - 155.0) + 101.0;
300- } else if (pm10 < 354.0) {
301- nowcast_10_0 = (200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0;
302- } else if (pm10 < 424.0) {
303- nowcast_10_0 = (300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0;
304- } else if (pm10 < 504.0) {
305- nowcast_10_0 = (400.0 - 301.0) / (504.0 - 425.0) * (pm10 - 425.0) + 301.0;
306- } else if (pm10 < 604) {
307- nowcast_10_0 = (500.0 - 401.0) / (604.0 - 505.0) * (pm10 - 505.0) + 401.0;
308- } else {
309- // everything higher is just counted as 500
310- nowcast_10_0 = 500.0;
311- }
302+ // Calculate the weight factor
303+ float range = max - min;
304+ float rate = range / max;
305+ float weight_factor = 1.0 - rate;
306+ if (weight_factor < 0.5) {
307+ weight_factor = 0.5;
308+ } else if (weight_factor > 1.0) {
309+ weight_factor = 1.0;
310+ }
311+
312+ float pm_sum = 0.0;
313+ float weight_sum = 0.0;
314+ for (int i = 0; i < hourly_10_0_size; i++) {
315+ float pm = id(pm_10_0_hourly_avg)[i];
316+ if (!isnan(pm) && pm > 0) {
317+ float weight_pow = pow(weight_factor, i);
318+ pm_sum += pm * weight_pow;
319+ weight_sum += weight_pow;
320+ }
321+ }
322+
323+ float pm10 = pm_sum / weight_sum;
324+ pm10 = static_cast<int>(pm10); // pm10 is truncated to the nearest whole number
312325
326+ // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
327+ // 2024 EPA breakpoints: https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
328+ if (pm10 < 54.0) {
329+ nowcast_10_0 = (50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0;
330+ } else if (pm10 < 154.0) {
331+ nowcast_10_0 = (100.0 - 51.0) / (154.0 - 55.0) * (pm10 - 55.0) + 51.0;
332+ } else if (pm10 < 254.0) {
333+ nowcast_10_0 = (150.0 - 101.0) / (254.0 - 155.0) * (pm10 - 155.0) + 101.0;
334+ } else if (pm10 < 354.0) {
335+ nowcast_10_0 = (200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0;
336+ } else if (pm10 < 424.0) {
337+ nowcast_10_0 = (300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0;
338+ } else if (pm10 < 500.0) {
339+ nowcast_10_0 = (500.0 - 301.0) / (500.0 - 425.0) * (pm10 - 425.0) + 301.0;
340+ } else {
341+ // everything higher is just counted as 500
342+ nowcast_10_0 = 500.0;
343+ }
344+
345+ }
313346 }
314347
315- int nowcast_calc = std::max( nowcast_2_5, nowcast_10_0) ;
348+ int nowcast_calc = nowcast_10_0 > nowcast_2_5 ? nowcast_10_0 : nowcast_2_5 ;
316349 if (nowcast_calc > 0) {
317350 id(nowcast).publish_state(nowcast_calc);
318351 // Just in case we're counting down, make sure we set to zero
319- if (id(nowcast_delay_mins) > 0) {
352+ if (isnan(id(nowcast_delay_mins)) || id(nowcast_delay_mins) > 0) {
320353 id(nowcast_delay_mins) = 0;
321354 id(nowcast_mins_remaining).publish_state(0);
322355 }
356+ } else {
357+ id(nowcast).publish_state(std::nan(""));
323358 }
324359
325360 - id : calculate_categories
@@ -377,11 +412,11 @@ interval:
377412 - interval : 1min
378413 then :
379414 - lambda : |
380- if (id(aqi_delay_mins) > 0) {
415+ if (!isnan(id(aqi_delay_mins)) && id(aqi_delay_mins) > 0) {
381416 id(aqi_delay_mins) -= 1;
382417 id(aqi_mins_remaining).publish_state(id(aqi_delay_mins));
383418 }
384- if (id(nowcast_delay_mins) > 0) {
419+ if (!isnan(id(nowcast_delay_mins)) && id(nowcast_delay_mins) > 0) {
385420 id(nowcast_delay_mins) -= 1;
386421 id(nowcast_mins_remaining).publish_state(id(nowcast_delay_mins));
387422 }
0 commit comments