Skip to content

Commit 3234cc5

Browse files
authored
Merge pull request #170 from ex-nerd/fix-nowcast
Fix NowCast and AQI glitches
2 parents 7e08b6a + cfc43d7 commit 3234cc5

File tree

1 file changed

+164
-129
lines changed

1 file changed

+164
-129
lines changed

packages/sensor_nowcast_aqi.yaml

Lines changed: 164 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)