Skip to content

Commit 9b2de6e

Browse files
committed
Fix NowCast and AQI glitches
- Start presenting NowCast after 2h (spec allows for this as long as 2 of the 3 most recent hours have data) - Truncate pm2.5 and pm10 sums per NowCast spec - Compensate for some array-out-of-bound index checks that were causing odd readings because C++ arrays don't prefill to null/NaN. - More isnan checks - Remove redundant set-to-zero that might have been related to #61
1 parent 7e08b6a commit 9b2de6e

File tree

1 file changed

+143
-114
lines changed

1 file changed

+143
-114
lines changed

packages/sensor_nowcast_aqi.yaml

Lines changed: 143 additions & 114 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,20 +123,22 @@ 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
}
132-
if (size_2_5 >= 18) {
133138
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-
}
139+
if (size_2_5 >= 18) {
138140
139-
float pm25 = sum / (float)size_2_5;
141+
float pm25 = sum_2_5 / (float)size_2_5;
140142
if (pm25 < 12.0) {
141143
aqi_2_5 = (50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0;
142144
} else if (pm25 < 35.4) {
@@ -156,18 +158,20 @@ script:
156158
}
157159
}
158160
159-
int size_10_0 = id(pm_10_0_hourly_avg).size();
160-
if (size_10_0 > 24) {
161-
size_10_0 = 24;
161+
int size_10_0 = 0;
162+
float sum_10_0 = 0.0;
163+
int loop_max_10 = std::min(24, static_cast<int>(id(pm_10_0_hourly_avg).size()));
164+
for (int i = 0; i < loop_max_10; i++) {
165+
float pm = id(pm_10_0_hourly_avg)[i];
166+
if (!isnan(pm) && pm > 0) {
167+
sum_10_0 += pm;
168+
size_10_0++;
169+
}
162170
}
163-
if (size_10_0 >= 18) {
164171
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-
}
172+
if (size_10_0 >= 18) {
169173
170-
float pm10 = sum / (float)size_10_0;
174+
float pm10 = sum_10_0 / (float)size_10_0;
171175
if (pm10 < 54.0) {
172176
aqi_10_0 = (50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0;
173177
} else if (pm10 < 154.0) {
@@ -191,8 +195,7 @@ script:
191195
if (aqi_calc > 0) {
192196
id(aqi).publish_state(aqi_calc);
193197
// 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) {
198+
if (isnan(id(aqi_delay_mins)) || id(aqi_delay_mins) > 0) {
196199
id(aqi_delay_mins) = 0;
197200
id(aqi_mins_remaining).publish_state(0);
198201
}
@@ -204,122 +207,148 @@ script:
204207
- lambda: |
205208
int nowcast_2_5 = -1;
206209
int nowcast_10_0 = -1;
210+
211+
int hourly_2_5_size = std::min(12, static_cast<int>(id(pm_2_5_hourly_avg).size()));
212+
int hourly_10_0_size = std::min(12, static_cast<int>(id(pm_10_0_hourly_avg).size()));
207213
208-
if (id(pm_2_5_hourly_avg).size() >= 12) {
214+
// nowcast is valid if we have 2 out of the last 3 hours worth of data
215+
if (hourly_2_5_size >= 2) {
209216
// Calculate min and max
210217
float max = 0.0;
211218
float min = 31337.0; // just a random large number
212-
for (int i = 0; i < 12; i++) {
219+
for (int i = 0; i < hourly_2_5_size; i++) {
213220
float pm = id(pm_2_5_hourly_avg)[i];
214-
if (pm < min) {
215-
min = pm;
216-
}
217-
if (pm > max) {
218-
max = pm;
221+
if (!isnan(pm) && pm > 0) {
222+
if (pm < min) {
223+
min = pm;
224+
}
225+
if (pm > max) {
226+
max = pm;
227+
}
219228
}
220229
}
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;
229-
}
230+
if (max >= min) {
230231
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;
232+
// Calculate the weight factor
233+
float range = max - min;
234+
float rate = range / max;
235+
float weight_factor = 1.0 - rate;
236+
if (weight_factor < 0.5) {
237+
weight_factor = 0.5;
238+
} else if (weight_factor > 1.0) {
239+
weight_factor = 1.0;
240+
}
239241
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;
242+
float pm_sum = 0.0;
243+
float weight_sum = 0.0;
244+
for (int i = 0; i < hourly_2_5_size; i++) {
245+
float pm = id(pm_2_5_hourly_avg)[i];
246+
if (!isnan(pm) && pm > 0) {
247+
float weight_pow = pow(weight_factor, i);
248+
pm_sum += pm * weight_pow;
249+
weight_sum += weight_pow;
250+
}
251+
}
252+
float pm25 = pm_sum / weight_sum;
253+
pm25 = static_cast<int>(pm25 * 10) / 10.0; // pm25 is truncated to the nearest 0.1
254+
255+
// https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
256+
if (pm25 < 12.0) {
257+
nowcast_2_5 = (50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0;
258+
} else if (pm25 < 35.4) {
259+
nowcast_2_5 = (100.0 - 51.0) / (35.4 - 12.1) * (pm25 - 12.1) + 51.0;
260+
} else if (pm25 < 55.4) {
261+
nowcast_2_5 = (150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0;
262+
} else if (pm25 < 150.4) {
263+
nowcast_2_5 = (200.0 - 151.0) / (150.4 - 55.5) * (pm25 - 55.5) + 151.0;
264+
} else if (pm25 < 250.4) {
265+
nowcast_2_5 = (300.0 - 201.0) / (250.4 - 150.5) * (pm25 - 150.5) + 201.0;
266+
} else if (pm25 < 350.4) {
267+
nowcast_2_5 = (400.0 - 301.0) / (350.4 - 250.5) * (pm25 - 250.5) + 301.0;
268+
} else if (pm25 < 500.4) {
269+
nowcast_2_5 = (500.0 - 401.0) / (500.4 - 350.5) * (pm25 - 350.5) + 401.0;
270+
} else {
271+
// everything higher is just counted as 500
272+
nowcast_2_5 = 500;
273+
}
274+
258275
}
259276
}
260277
261-
if (id(pm_10_0_hourly_avg).size() >= 12) {
278+
if (hourly_10_0_size >= 2) {
262279
// Calculate min and max
263280
float max = 0.0;
264281
float min = 31337.0; // just a random large number
265-
for (int i = 0; i < 12; i++) {
282+
for (int i = 0; i < hourly_10_0_size; i++) {
266283
float pm = id(pm_10_0_hourly_avg)[i];
267-
if (pm < min) {
268-
min = pm;
269-
}
270-
if (pm > max) {
271-
max = pm;
284+
if (!isnan(pm) && pm > 0) {
285+
if (pm < min) {
286+
min = pm;
287+
}
288+
if (pm > max) {
289+
max = pm;
290+
}
272291
}
273292
}
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-
}
283293
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;
294+
if (max >= min) {
292295
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-
}
296+
// Calculate the weight factor
297+
float range = max - min;
298+
float rate = range / max;
299+
float weight_factor = 1.0 - rate;
300+
if (weight_factor < 0.5) {
301+
weight_factor = 0.5;
302+
} else if (weight_factor > 1.0) {
303+
weight_factor = 1.0;
304+
}
312305
306+
float pm_sum = 0.0;
307+
float weight_sum = 0.0;
308+
for (int i = 0; i < hourly_10_0_size; i++) {
309+
float pm = id(pm_10_0_hourly_avg)[i];
310+
if (!isnan(pm) && pm > 0) {
311+
float weight_pow = pow(weight_factor, i);
312+
pm_sum += pm * weight_pow;
313+
weight_sum += weight_pow;
314+
}
315+
}
316+
float pm10 = pm_sum / weight_sum;
317+
pm10 = static_cast<int>(pm10); // pm10 is truncated to the nearest whole number
318+
319+
// https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
320+
if (pm10 < 54.0) {
321+
nowcast_10_0 = (50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0;
322+
} else if (pm10 < 154.0) {
323+
nowcast_10_0 = (100.0 - 51.0) / (154.0 - 55.0) * (pm10 - 55.0) + 51.0;
324+
} else if (pm10 < 254.0) {
325+
nowcast_10_0 = (150.0 - 101.0) / (254.0 - 155.0) * (pm10 - 155.0) + 101.0;
326+
} else if (pm10 < 354.0) {
327+
nowcast_10_0 = (200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0;
328+
} else if (pm10 < 424.0) {
329+
nowcast_10_0 = (300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0;
330+
} else if (pm10 < 504.0) {
331+
nowcast_10_0 = (400.0 - 301.0) / (504.0 - 425.0) * (pm10 - 425.0) + 301.0;
332+
} else if (pm10 < 604) {
333+
nowcast_10_0 = (500.0 - 401.0) / (604.0 - 505.0) * (pm10 - 505.0) + 401.0;
334+
} else {
335+
// everything higher is just counted as 500
336+
nowcast_10_0 = 500.0;
337+
}
338+
339+
}
313340
}
314341
315-
int nowcast_calc = std::max(nowcast_2_5, nowcast_10_0);
342+
int nowcast_calc = nowcast_10_0 > nowcast_2_5 ? nowcast_10_0 : nowcast_2_5;
316343
if (nowcast_calc > 0) {
317344
id(nowcast).publish_state(nowcast_calc);
318345
// Just in case we're counting down, make sure we set to zero
319-
if (id(nowcast_delay_mins) > 0) {
346+
if (isnan(id(nowcast_delay_mins)) || id(nowcast_delay_mins) > 0) {
320347
id(nowcast_delay_mins) = 0;
321348
id(nowcast_mins_remaining).publish_state(0);
322349
}
350+
} else {
351+
id(nowcast).publish_state(std::nan(""));
323352
}
324353
325354
- id: calculate_categories
@@ -377,11 +406,11 @@ interval:
377406
- interval: 1min
378407
then:
379408
- lambda: |
380-
if (id(aqi_delay_mins) > 0) {
409+
if (!isnan(id(aqi_delay_mins)) && id(aqi_delay_mins) > 0) {
381410
id(aqi_delay_mins) -= 1;
382411
id(aqi_mins_remaining).publish_state(id(aqi_delay_mins));
383412
}
384-
if (id(nowcast_delay_mins) > 0) {
413+
if (!isnan(id(nowcast_delay_mins)) && id(nowcast_delay_mins) > 0) {
385414
id(nowcast_delay_mins) -= 1;
386415
id(nowcast_mins_remaining).publish_state(id(nowcast_delay_mins));
387416
}

0 commit comments

Comments
 (0)