No harm in lowering the interval. The watchdog needs to be activated within a certain time depending on the resistor connected. I calculated its about 8min but yours could be shorter. As long as you feed it in time it will not reset.
Update on the JSON stuff: I tracked down the official AirGradient Outdoor code and confirmed the JSON format is indeed sending an object with keys of 1
and 2
. And as far as I can tell, that Must be a string
error is a “feature” of espHome itself: it just doesn’t supported nested properties via the json
option.
The code also showed that it should be sending the full MAC address in the URL, so I’ve restored that in my copy. I’ll find some time this weekend to fork the repo and get my changes onto github rather than lazily relying on my clipboard history as version control. I also have some updates for the DIY Pro sensor firmware that I want to get in place.
I’m not so sure on the full MAC. It looks like the ESP.getChipId() function returns the last 3 bytes in HEX and not the full MAC
arduino-esp32/GetChipID.ino at master · espressif/arduino-esp32 · GitHub
I haven’t played with ESPHome with a JSON that has multiple keys, but there may be some YAML formatting to have it format more as expected. I haven’t tried any of this, but in YAML, it would be something like
channels:
- 1:
pm01: !lambda return to_string(id(pm1_1_0).state);
pm02: !lambda return to_string(id(pm1_2_5).state);
pm10: !lambda return to_string(id(pm1_10_0).state);
pm003_count: !lambda return to_string(id(pm1_0_3um).state);
atmp: !lambda return to_string(id(pm1_temperature).state);
rhum: !lambda return to_string(id(pm1_humidity).state);
- 2:
pm01: !lambda return to_string(id(pm2_1_0).state);
pm02: !lambda return to_string(id(pm2_2_5).state);
pm10: !lambda return to_string(id(pm2_10_0).state);
Or maybe not naming the keys, but just making it a list:
channels:
- pm01: !lambda return to_string(id(pm1_1_0).state);
pm02: !lambda return to_string(id(pm1_2_5).state);
pm10: !lambda return to_string(id(pm1_10_0).state);
pm003_count: !lambda return to_string(id(pm1_0_3um).state);
atmp: !lambda return to_string(id(pm1_temperature).state);
rhum: !lambda return to_string(id(pm1_humidity).state);
- pm01: !lambda return to_string(id(pm2_1_0).state);
pm02: !lambda return to_string(id(pm2_2_5).state);
pm10: !lambda return to_string(id(pm2_10_0).state);
Oh, that was totally an assumption on my part that it would return the whole value. Thanks for catching that.
Yeah, it’s not the YAML itself that’s problematic, but the validator for the json
property is set as “strict string”: esphome/config_validation.py at 8bb4c65272b41ccc6fa25a9e39096617c8c041c5 · esphome/esphome · GitHub
I spent way too much time poking at this, but it’s been a good leaning project for ESPHome. My version of the file is now on github (link at the end of this post). I haven’t yet submitted an upstream pull request because I’m waiting a couple days for AQI and NowCast to show up and make sure they’re consistent and the system is stable.
Major rewrite: AQI + NowCast
I now have a better understanding if AQI, and see why it’s something
that needs to be a “once/day” calculation. To that end, I’ve added
NowCast calculations, which require keeping track of the hourly
averages. This actually paves the way for storing the values in
flash so they can be restored across reboots and speed up
re-calculation (though we’ll need to track the hours that each avg
comes from so we can age out too-old values).
- Rewrite AQI calculation based on official standards.
- No more separate 2.5 and 10 variants. AQI is AQI, and there is no
reason to provide the separate pseudo-AQI calculations. Plus, it
was a lot of duplcated code for little benefit. - Document why we use 18 hours not 24: the official spec allows for
calculations once we hit 75% data availability. - Note: This calculation still differs from the official AQI , which
is apparently calculated midnight-to-midnight rather than a 24h
rolling average.
- No more separate 2.5 and 10 variants. AQI is AQI, and there is no
- Add NowCast. This is a 12h version of AQI weighted to the most recent
hour, and is a better fit for what a lot of people want from AQI: a
measurement of air quality during short-term quality issues like
wildfire smoke. - Add sensors to report on the number of minutes remaining before AQI
and NowCast are available. - Update some sensors to report 1 accuracy decimal instead of 0.
- Changes around a few icons.
- Fixes some math and timing values.
- More inline comments/documentation.
Also includes previous work:
- Make initial_aqi_delay configurable. I’ve arbitrarily chosen 4h here, and
have some ideas how to make it better in the future because I like seeing
numbers and can trust myself to know it’s not particularly accurate before
24h. - Turned on the captive portal (fallback hotspot is kind of useless without
it) - Included a section for Home Assistant API and encryption key (stored as a
secret) - Separated AQI into pm2.5, pm10.0, and a generic AQI that matches whichever
value is highest. Only the “best of” AQI values are sent to Home Assistant
by default. - Set a bunch of the values to disabled_by_default: true so only the
combined/average values show up by default in Home Assistant. - Extended the AQI calculations to the maximum of 500 if particle readings
are REALLY bad (hello west coast wildfiles … I know some people who were
caught in areas “above 500”). - Tried to clean up the AQI calculations (both numeric and text) to be
easier to read/maintain. - Switched around a few of the icons:
- mdi:blur seems to be a popular one for particle count
- Home Assistant seems to use the plain mdi:thermometer icon for
temperatures - Use mdi:weather-windy-variant for AQI, which matches Home Assistant’s
use of it for the air_quality integration.
Wow, you really outdid yourself. When I did the AQI calculation a year ago I just settled for that 18h and didnt want to bother with calculating provisional numbers. And just build up data after that initial start. I still had issues with spikes in the input sensor which causes the AQI to bump hours later which should be compensated for I think.
I will try your code both on inside and outside unit soon.
Make sure to pull the latest. I found some issues after my original changes. Things appear to be stable and calculating properly now.
I also have a bit of cleanup for the DIY Pro version (spent way too much time finding better-looking fonts) but will find an appropriate thread to post about those updates.
And in case anyone is curious, here’s the card I’m using in Home Assistant. It uses stack-in-card
, card-mod
, and mini-graph-card
from HACS.
type: custom:stack-in-card
mode: vertical
keep:
background: true
border_radius: false
margin: false
cards:
- type: markdown
content: >
{%- set category =
states('sensor.airgradient_open_air_airgradient_open_air_nowcast_category')
-%}
{% if category == "unknown" %} # NowCast: Calculating
{% set mins =
states('sensor.airgradient_open_air_nowcast_minutes_remaining')
| int %} Available in {% if mins > 60 %}{{ mins // 60 }} hours {% endif
%}{{ mins%60 }} minutes
{% else %}
# NowCast: {{ category }}
{% endif %}
card_mod:
style: |
ha-card {
{% set aqi = states('sensor.airgradient_open_air_airgradient_open_air_nowcast') -%}
{%- set aqi = aqi | float if is_number(aqi) else -1 -%}
{%- if aqi < 0 -%}
background-color: #a0a0a0; color: #393939;
{%- elif aqi < 50 -%}
background-color: #00e400; color: #004000;
{%- elif aqi < 100 -%}
background-color: #ffff00; color: #404000;
{%- elif aqi < 150 -%}
background-color: #ff0000; color: #400000;
{%- elif aqi < 200 -%}
background-color: #8f3f97; color: #300030;
{%- else -%}
background-color: #7e0023; color: #200000;
{% endif %}
text-align: center;
}
h1 {
line-height: 1px;
margin: 0 !important;
padding: 0 !important;
border: 1px solid red !impotant;
}
- type: conditional
conditions:
- entity: sensor.airgradient_open_air_airgradient_open_air_nowcast
state_not: unknown
card:
type: gauge
entity: sensor.airgradient_open_air_airgradient_open_air_nowcast
needle: true
min: 0
max: 500
segments:
- from: 0
color: '#00e400'
- from: 50
color: '#ffff00'
- from: 100
color: '#ff7e00'
- from: 150
color: '#ff0000'
- from: 200
color: '#8f3f97'
- from: 300
color: '#7e0023'
name: ''
- type: custom:mini-graph-card
entities:
- entity: sensor.airgradient_open_air_airgradient_open_air_nowcast
color_thresholds:
- value: 0
color: '#00e400'
- value: 50
color: '#ffff00'
- value: 100
color: '#ff0000'
- value: 200
color: '#8f3f97'
- value: 300
color: '#7e0023'
show:
labels: false
fill: fade
name: false
state: true
icon: false
min_bound_range: 20
line_width: 4
hours_to_show: 24
points_per_hour: 1
card_mod:
style: |
div.states {
display: none !important;
position: absolute;
bottom: 0;
left: 0;
}
ha-card:hover div.states {
display:block !important;
}
- type: markdown
content: |
{%- set category =
states('sensor.airgradient_open_air_airgradient_open_air_aqi_category')
-%} {% if category == "unknown" %} # AQI: Calculating
{% set mins = states('sensor.airgradient_open_air_aqi_minutes_remaining')
| int %} Available in {% if mins > 60 %}{{ mins // 60 }} hours {% endif
%}{{ mins%60 }} minutes
{% else %} # AQI: {{ category }} {% endif %}
card_mod:
style: |
ha-card {
{% set aqi = states('sensor.airgradient_open_air_airgradient_open_air_aqi') -%}
{%- set aqi = aqi | float if is_number(aqi) else -1 -%}
{%- if aqi < 0 -%}
background-color: #a0a0a0; color: #393939;
{%- elif aqi < 50 -%}
background-color: #00e400; color: #004000;
{%- elif aqi < 100 -%}
background-color: #ffff00; color: #404000;
{%- elif aqi < 150 -%}
background-color: #ff0000; color: #400000;
{%- elif aqi < 200 -%}
background-color: #8f3f97; color: #300030;
{%- else -%}
background-color: #7e0023; color: #200000;
{% endif %}
text-align: center;
}
h1 {
line-height: 1px;
margin: 0 !important;
padding: 0 !important;
border: 1px solid red !impotant;
}
- type: conditional
conditions:
- entity: sensor.airgradient_open_air_airgradient_open_air_aqi
state_not: unknown
card:
type: gauge
entity: sensor.airgradient_open_air_airgradient_open_air_aqi
needle: true
min: 0
max: 500
segments:
- from: 0
color: '#00e400'
- from: 50
color: '#ffff00'
- from: 100
color: '#ff7e00'
- from: 150
color: '#ff0000'
- from: 200
color: '#8f3f97'
- from: 300
color: '#7e0023'
name: ''
- type: custom:mini-graph-card
entities:
- entity: sensor.airgradient_open_air_airgradient_open_air_aqi
color_thresholds:
- value: 0
color: '#00e400'
- value: 50
color: '#ffff00'
- value: 100
color: '#ff0000'
- value: 200
color: '#8f3f97'
- value: 300
color: '#7e0023'
show:
labels: false
fill: fade
name: false
state: true
icon: false
min_bound_range: 20
line_width: 4
hours_to_show: 24
points_per_hour: 1
card_mod:
style: |
div.states {
display: none !important;
position: absolute;
bottom: 0;
left: 0;
}
ha-card:hover div.states {
display:block !important;
}
- type: horizontal-stack
cards:
- type: custom:mini-graph-card
entities:
- entity: >-
sensor.airgradient_open_air_airgradient_open_air_particulate_matter_1_0um_concentration
name: PM1
state_adaptive_color: true
color: wheat
line_width: 2
hours_to_show: 24
min_bound_range: 10
show:
labels: false
fill: fade
name: true
icon: false
name_adaptive_color: true
- type: custom:mini-graph-card
entities:
- entity: >-
sensor.airgradient_open_air_airgradient_open_air_particulate_matter_2_5um_concentration
name: PM2.5
state_adaptive_color: true
color: tan
line_width: 2
hours_to_show: 24
min_bound_range: 10
show:
labels: false
fill: fade
name: true
icon: false
name_adaptive_color: true
- type: custom:mini-graph-card
entities:
- entity: >-
sensor.airgradient_open_air_airgradient_open_air_particulate_matter_10_0um_concentration
name: PM10
state_adaptive_color: true
color: burlywood
line_width: 2
hours_to_show: 24
min_bound_range: 10
show:
labels: false
fill: fade
name: true
icon: false
name_adaptive_color: true
columns: 1
For those following along, I submitted a PR for my changes to both the Outdoor/Open Air and DIY Pro models: Quality of life and other (somewhat major) updates for AirGradient Outdoor and DIY Pro by ex-nerd · Pull Request #17 · ajfriesen/ESPHome-AirGradient · GitHub
Is there a way to save/restore 24 hours of data so that on a reboot it doesn’t have the delay for AQI and Now Cast?
It’s possible to store numbers on the flash on the device, but the info I found basically says it has about 10k writes before it starts to degrade. With hourly updates, that’s about a year and a half before the device stops working properly.
Others are doing the calculation in Home Assistant itself, but I haven’t had time to look into how to actually query past/logged states and build that into a value that can be added to a dashboard.
Great to see such an active discussion on the new outdoor monitor and ESPHome integrations.
The flash chip on the C3 is indeed limited with write cycles and I would also recommend to offload the calculation to the server and only transmitting raw data. This gives more flexibility to show also the last current measured data to show trends etc.
I received my outdoor kit last week and have been testing this esphome configuration since then and other than the 18h/12h delays it has been working well.
My problem has been that a few very small power interruptions return the 18h/12h delays on the AQI making it hard to rely on the value.
I also experienced reboots (crashes?) when the device can’t upload to airgradient (internet outage or firewall block) which again end up restarting the counter.
I finally commented out the upload to airgradient and added the calculations to home-assistant. The AQI was pretty straightforward using a 24h statistic sensor and template sensors. But the NowCast is complex to do in Home-Assistant using the official calculations. After testing a few scenarios it seems that using a 3h statistics sensor with the AQI calculations return values that are very close to the NowCast values returned by Esphome.
*The AQI is off at the beginning of the graph because of a 18h/12h reset
Regarding the query for statistics from Home-Assistant, this can be done using the websocket API. I don’t think it is documented but the relevant code in Home-Assistant is here
If any one is interested here are my configurations for Home-Assistant sensors
## AQI 3h
sensor:
- platform: statistics
name: PM2.5 Mean 3h
unique_id: pm2_5_mean_3h
entity_id: sensor.airgradient_airgradient_open_air_particulate_matter_2_5um_concentration
state_characteristic: mean
max_age:
hours: 3
- platform: statistics
name: PM10 Mean 3h
unique_id: pm10_mean_3h
entity_id: sensor.airgradient_airgradient_open_air_particulate_matter_10_0um_concentration
state_characteristic: mean
max_age:
hours: 3
- platform: statistics
name: PM2.5 Mean 24h
unique_id: pm2_5_mean_24h
entity_id: sensor.airgradient_airgradient_open_air_particulate_matter_2_5um_concentration
state_characteristic: mean
max_age:
hours: 24
- platform: statistics
name: PM10 Mean 24h
unique_id: pm10_mean_24h
entity_id: sensor.airgradient_airgradient_open_air_particulate_matter_10_0um_concentration
state_characteristic: mean
max_age:
hours: 24
template:
- sensor:
- name: "AQI EPA PM2.5 3h"
unique_id: aqi_epa_pm25_3h
device_class: aqi
state: >
{% set pm25 = states('sensor.pm2_5_mean_3h')|float %}
{% if pm25 < 12.0 %}
{{ ((50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0)|round }}
{% elif pm25 < 35.4 %}
{{ ((100.0 - 51.0) / (35.4 - 12.1) * (pm25 - 12.1) + 51.0)|round }}
{% elif pm25 < 55.4 %}
{{ ((150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0)|round }}
{% elif pm25 < 150.4 %}
{{ ((200.0 - 151.0) / (150.4 - 55.5) * (pm25 - 55.5) + 151.0)|round }}
{% elif pm25 < 250.4 %}
{{ ((300.0 - 201.0) / (250.4 - 150.5) * (pm25 - 150.5) + 201.0)|round }}
{% elif pm25 < 350.4 %}
{{ ((400.0 - 301.0) / (350.4 - 250.5) * (pm25 - 250.5) + 301.0)|round }}
{% elif pm25 < 500.4 %}
{{ ((500.0 - 401.0) / (500.4 - 350.5) * (pm25 - 350.5) + 401.0)|round }}
{% else %}
500
{% endif %}
- name: "AQI EPA PM10 3h"
unique_id: aqi_epa_pm10_3h
device_class: aqi
state: >
{% set pm10 = states('sensor.pm10_mean_3h')|float %}
{% if pm10 < 54.0 %}
{{ ((50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0)|round }}
{% elif pm10 < 154.0 %}
{{ ((100.0 - 51.0) / (154.0 - 55.0) * (pm10 - 55.0) + 51.0)|round }}
{% elif pm10 < 254.0 %}
{{ ((150.0 - 101.0) / (254.0 - 155.0) * (pm10 - 155.0) + 101.0)|round }}
{% elif pm10 < 354.0 %}
{{ ((200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0)|round }}
{% elif pm10 < 424.0 %}
{{ ((300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0)|round }}
{% elif pm10 < 504.0 %}
{{ ((400.0 - 301.0) / (504.0 - 425.0) * (pm10 - 425.0) + 301.0)|round }}
{% elif pm10 < 604.0 %}
{{ ((500.0 - 401.0) / (604.0 - 505.0) * (pm10 - 505.0) + 401.0)|round }}
{% else %}
500
{% endif %}
- name: "AQI EPA 3h"
unique_id: aqi_epa_3h
device_class: aqi
state: >-
{{ [states('sensor.aqi_epa_pm10_3h')|int, states('sensor.aqi_epa_pm2_5_3h')|int]|max }}
- name: "AQI EPA Category 3h"
unique_id: aqi_epa_category_3h
state: >-
{% set aqi = states('sensor.aqi_epa_3h')|int %}
{% if aqi <= 50.0 %}
"Good"
{% elif aqi <= 100.0 %}
"Moderate"
{% elif aqi <= 150.0 %}
"Unhealthy for Sensitive Groups"
{% elif aqi <= 200.0 %}
"Unhealthy"
{% elif aqi <= 300.0 %}
"Very Unhealthy"
{% elif aqi <= 500.0 %}
"Hazardous"
{% else %}
"Extreme Hazardous"
{% endif %}
## AQI EPA 24h
- name: AQI EPA PM2.5
unique_id: aqi_epa_pm25
device_class: aqi
state: >
{% set pm25 = states('sensor.pm2_5_mean_24h')|float %}
{% if pm25 < 12.0 %}
{{ ((50.0 - 0.0) / (12.0 - 0.0) * (pm25 - 0.0) + 0.0)|round }}
{% elif pm25 < 35.4 %}
{{ ((100.0 - 51.0) / (35.4 - 12.1) * (pm25 - 12.1) + 51.0)|round }}
{% elif pm25 < 55.4 %}
{{ ((150.0 - 101.0) / (55.4 - 35.5) * (pm25 - 35.5) + 101.0)|round }}
{% elif pm25 < 150.4 %}
{{ ((200.0 - 151.0) / (150.4 - 55.5) * (pm25 - 55.5) + 151.0)|round }}
{% elif pm25 < 250.4 %}
{{ ((300.0 - 201.0) / (250.4 - 150.5) * (pm25 - 150.5) + 201.0)|round }}
{% elif pm25 < 350.4 %}
{{ ((400.0 - 301.0) / (350.4 - 250.5) * (pm25 - 250.5) + 301.0)|round }}
{% elif pm25 < 500.4 %}
{{ ((500.0 - 401.0) / (500.4 - 350.5) * (pm25 - 350.5) + 401.0)|round }}
{% else %}
500
{% endif %}
- name: "AQI EPA PM10"
unique_id: aqi_epa_pm10
device_class: aqi
state: >
{% set pm10 = states('sensor.pm10_mean_24h')|float %}
{% if pm10 < 54.0 %}
{{ ((50.0 - 0.0) / (54.0 - 0.0) * (pm10 - 0.0) + 0.0)|round }}
{% elif pm10 < 154.0 %}
{{ ((100.0 - 51.0) / (154.0 - 55.0) * (pm10 - 55.0) + 51.0)|round }}
{% elif pm10 < 254.0 %}
{{ ((150.0 - 101.0) / (254.0 - 155.0) * (pm10 - 155.0) + 101.0)|round }}
{% elif pm10 < 354.0 %}
{{ ((200.0 - 151.0) / (354.0 - 255.0) * (pm10 - 255.0) + 151.0)|round }}
{% elif pm10 < 424.0 %}
{{ ((300.0 - 201.0) / (424.0 - 355.0) * (pm10 - 355.0) + 201.0)|round }}
{% elif pm10 < 504.0 %}
{{ ((400.0 - 301.0) / (504.0 - 425.0) * (pm10 - 425.0) + 301.0)|round }}
{% elif pm10 < 604.0 %}
{{ ((500.0 - 401.0) / (604.0 - 505.0) * (pm10 - 505.0) + 401.0)|round }}
{% else %}
500
{% endif %}
- name: "AQI EPA"
unique_id: aqi_epa
device_class: aqi
state: >-
{{ [states('sensor.aqi_epa_pm10')|int, states('sensor.aqi_epa_pm2_5')|int]|max }}
- name: "AQI EPA Category "
unique_id: aqi_epa_category
state: >-
{% set aqi = states('sensor.aqi_epa')|int %}
{% if aqi <= 50.0 %}
"Good"
{% elif aqi <= 100.0 %}
"Moderate"
{% elif aqi <= 150.0 %}
"Unhealthy for Sensitive Groups"
{% elif aqi <= 200.0 %}
"Unhealthy"
{% elif aqi <= 300.0 %}
"Very Unhealthy"
{% elif aqi <= 500.0 %}
"Hazardous"
{% else %}
"Extreme Hazardous"
{% endif %}
Great work, thanks for the info. I’ve been looking into the SQL plugin to get at the raw data to do the AQI and NowCast stuff but this is certainly easier. I’ll probably still do a deeper dive into this because I want to learn the APIs and get an “actual proper calculation” written (just for the sake of doing it … I think the official midnight-to-midnight AQI isn’t helpful within a home assistant context) but this really helps me get a head start on how to get at some of this info.
I started looking into the AQI calculations based on the SQL database itself, and discovered that the Home Assistant database seems to be missing a lot of readings from the AirGradient. I’m not sure if this is something about HA, or if the AirGradient device itself is skipping readings, but it’s definitely another kink to work out in getting everything running.
You’ll obviously need to adjust the sensor ID to match yours, but here’s some sample SQL you can run directly against the sqlite database (I run HA in a docker container, not sure how to access the raw db if you run HA-OS). It returns the average state and number of readings for every hour (should be a reading every 3 minutes, so 20 per hour).
SELECT
datetime(FLOOR(AVG(states.last_updated_ts)/3600) * 3600, 'unixepoch') AS by_hour,
COUNT(*) AS count,
AVG(states.state) AS state
FROM
states
INNER JOIN states_meta ON
states.metadata_id = states_meta.metadata_id
-- LEFT JOIN state_attributes ON
-- states.attributes_id = state_attributes.attributes_id
WHERE
-- states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_2_5um_concentration'
states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_2_5um_concentration_1'
-- OR states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_2_5um_concentration_2'
-- states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_10_0um_concentration'
-- states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_10_0um_concentration_1'
-- OR states_meta.entity_id = 'sensor.airgradient_open_air_airgradient_open_air_particulate_matter_10_0um_concentration_2'
AND states.state != 'unavailable'
AND states.last_updated_ts >= unixepoch(date('now','start of day','-1 day'))
-- AND states.last_updated_ts < unixepoch(date('now','start of day'))
GROUP BY
floor(states.last_updated_ts/3600) -- group by hour
ORDER BY
states.last_updated_ts DESC
Replace 3600
with 60
to group things by minute , which makes it pretty easy to see where there are gaps.
I’ve been watching the on-device logs through its web console and I don’t see any errors or skipped time codes, so I suspect this is a communication issue with esphome and home assistant, rather than something blocking the readings on the device itself.
Anyway, just an annoyance to work through.
After a bit more digging, it seems this is “a feature, not a bug” … meaning that HA only logs sensor states when the value changes. Annoyingly (I say this as a sometimes DBA) they don’t even track the created/updated timestamps so will have to do a bit more db magic to make this all work.
I am using your repo for the unit and noticed I am getting resets according to Home Assistant.
Here is the reset reason:
Timer Group 0 Watch Dog Reset Digital Core
Here is what I am seeing from Home Assistant as to the uptime:
Any thoughts on what to tweak or check out? Thanks!
I don’t. Mine seems to do that when it has trouble reaching the internet. I started digging into an SQL query for home assistant to move these calculations there, but … life got in the way and the on-device calculations are good enough most of the time.
The calculations wouldn’t be particularly difficult but HA only stores a new value when/if it changes so it makes things more complicated to “fill in” values from the hours when nothing changes.
I also just noticed that someone submitted a pull request that should help make some statistics queries easier in home assistant, so I merged that.
See this thread that is likely related, regarding esphome and reboots with the PMS sensor with default settings
I just read your issue with not having all the data in HA. That is because by default it will only store changed values to save on storage. In esphome its possible to force it to update with force_update: true on a sensor.
Because I copy my data to influx I let Grafana ‘fix’ the missing data points so I have best of both worlds.