33substitutions :
44 devicename : " ag-basic"
55 upper_devicename : " AG Basic"
6+ ag_esphome_config_version : 0.1.1
67
78 # Instructions: https://esphome.io/guides/getting_started_hassio.html
89esphome :
@@ -14,24 +15,48 @@ esp8266:
1415 board : d1_mini
1516
1617# Enable logging
18+ # https://esphome.io/components/logger.html
1719logger :
20+ logs :
21+ component : ERROR # Hiding warning messages about component taking a long time https://github.com/esphome/issues/issues/4717
1822
1923# Enable Home Assistant API
2024api :
21- password : " "
2225
2326ota :
24- password : " "
2527
2628wifi :
27- # https://esphome.io/components/wifi.html
28- ssid : !secret wifi_ssid
29- password : !secret wifi_password
3029 # Enable fallback hotspot (captive portal) in case wifi connection fails
31- # Defaults to device name with no password
32- ap : {}
30+ ap :
31+
32+ # The captive portal is a fallback mechanism for when connecting to the configured WiFi fails.
33+ # https://esphome.io/components/captive_portal.html
3334captive_portal :
3435
36+ http_request :
37+ # Used to support POST request to send data to AirGradient
38+ # https://esphome.io/components/http_request.html
39+
40+ switch :
41+ - platform : safe_mode
42+ # Create a switch for safe_mode in order to flash the device
43+ # Solution from this thread:
44+ # https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1
45+ name : " Flash Mode (Safe Mode)"
46+ icon : " mdi:cellphone-arrow-down"
47+
48+ - platform : template
49+ name : " Display Temperature in °F"
50+ icon : " mdi:thermometer"
51+ id : display_in_f
52+ restore_mode : RESTORE_DEFAULT_ON
53+ optimistic : True
54+
55+ - platform : template
56+ name : " Upload to AirGradient Dashboard"
57+ id : upload_airgradient
58+ restore_mode : RESTORE_DEFAULT_ON
59+ optimistic : True
3560
3661uart :
3762 # https://esphome.io/components/uart.html#uart
5176
5277sensor :
5378 - platform : pmsx003
54- # https://esphome.io/components/sensor/pmsx003.html
79+ # Default interval of updating every second, but using an average over the last 30 seconds/readings
80+ # PMS5003 https://esphome.io/components/sensor/pmsx003.html
5581 type : PMSX003
82+ uart_id : pms5003_uart
5683 pm_2_5 :
57- name : " PM <2.5µm Concentration "
84+ name : " PM 2.5 "
5885 id : pm_2_5
59- on_value :
60- lambda : |-
61- // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
62- // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
63- if (id(pm_2_5).state <= 12.0) {
64- // good
65- id(pm_2_5_aqi).publish_state((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
66- } else if (id(pm_2_5).state <= 35.4) {
67- // moderate
68- id(pm_2_5_aqi).publish_state((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
69- } else if (id(pm_2_5).state <= 55.4) {
70- // usg
71- id(pm_2_5_aqi).publish_state((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
72- } else if (id(pm_2_5).state <= 150.4) {
73- // unhealthy
74- id(pm_2_5_aqi).publish_state((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
75- } else if (id(pm_2_5).state <= 250.4) {
76- // very unhealthy
77- id(pm_2_5_aqi).publish_state((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
78- } else if (id(pm_2_5).state <= 350.4) {
79- // hazardous
80- id(pm_2_5_aqi).publish_state((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
81- } else if (id(pm_2_5).state <= 500.4) {
82- // hazardous 2
83- id(pm_2_5_aqi).publish_state((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
84- } else {
85- id(pm_2_5_aqi).publish_state(500);
86- }
86+ filters :
87+ - sliding_window_moving_average :
88+ window_size : 30
89+ send_every : 30
8790 pm_1_0 :
88- name : " PM <1.0µm Concentration "
91+ name : " PM 1.0 "
8992 id : pm_1_0
93+ filters :
94+ - sliding_window_moving_average :
95+ window_size : 30
96+ send_every : 30
9097 pm_10_0 :
91- name : " PM < 10.0µm Concentration "
98+ name : " PM 10.0 "
9299 id : pm_10_0
100+ filters :
101+ - sliding_window_moving_average :
102+ window_size : 30
103+ send_every : 30
93104 pm_0_3um :
94- name : " PM >0.3µm Count "
105+ name : " PM 0.3 "
95106 id : pm_0_3um
96- update_interval : 2min
97- uart_id : pms5003_uart
107+ filters :
108+ - sliding_window_moving_average :
109+ window_size : 30
110+ send_every : 30
98111
99112 - platform : template
100- name : " PM <2.5 AQI"
113+ # Depends on another sensor providing an ID of pm_2_5 such as a pms5003
114+ name : " PM 2.5 AQI"
115+ id : pm_2_5_aqi
116+ update_interval : 5 min
101117 unit_of_measurement : " AQI"
102118 icon : " mdi:air-filter"
103119 accuracy_decimals : 0
104- id : pm_2_5_aqi
120+ filters :
121+ - skip_initial : 10 # Need valid data from PM 2.5 sensor before able to calculate
122+ lambda : |-
123+ // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI
124+ // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml
125+ if (id(pm_2_5).state <= 12.0) {
126+ // good
127+ return((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0);
128+ } else if (id(pm_2_5).state <= 35.4) {
129+ // moderate
130+ return((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0);
131+ } else if (id(pm_2_5).state <= 55.4) {
132+ // usg
133+ return((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0);
134+ } else if (id(pm_2_5).state <= 150.4) {
135+ // unhealthy
136+ return((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0);
137+ } else if (id(pm_2_5).state <= 250.4) {
138+ // very unhealthy
139+ return((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0);
140+ } else if (id(pm_2_5).state <= 350.4) {
141+ // hazardous
142+ return((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0);
143+ } else if (id(pm_2_5).state <= 500.4) {
144+ // hazardous 2
145+ return((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0);
146+ } else {
147+ return(500);
148+ }
105149
106150 - platform : senseair
107- # https://esphome.io/components/sensor/senseair.html
151+ # SenseAir S8 https://esphome.io/components/sensor/senseair.html
152+ # https://senseair.com/products/size-counts/s8-lp/
108153 co2 :
109- name : " SenseAir S8 CO2 Value "
154+ name : " SenseAir S8 CO2"
110155 id : co2
111156 filters :
112157 - skip_initial : 2
113158 - clamp :
114159 min_value : 400 # 419 as of 2023-06 https://gml.noaa.gov/ccgg/trends/global.html
115- update_interval : 30s
116160 id : senseair_s8
117161 uart_id : senseair_s8_uart
118162
119163 # - platform: sht4x
120- # # SHT41 https://esphome.io/components/sensor/sht4x.html
164+ # # SHT40 https://esphome.io/components/sensor/sht4x.html
121165 # temperature:
122166 # name: "Temperature"
123167 # id: temp
@@ -135,34 +179,17 @@ sensor:
135179 name : " Humidity"
136180 id : humidity
137181 address : 0x44
138- heater_enabled : false # Only enable if in conditions that may have high condensation
139182
140183 - platform : wifi_signal
141184 name : " WiFi Signal"
142185 id : wifi_dbm
143186 update_interval : 60s
144187
145188 - platform : uptime
146- name : Uptime Sensor
189+ name : " Uptime"
147190 id : device_uptime
148191 update_interval : 10s
149192
150- # - platform: sgp30
151- # # https://esphome.io/components/sensor/sgp30.html
152- # # Unable to get to work when OLED shield is also connected
153- # eco2:
154- # name: "eCO2"
155- # id: eco2
156- # accuracy_decimals: 1
157- # tvoc:
158- # name: "TVOC"
159- # id: tvoc
160- # accuracy_decimals: 1
161- # update_interval: 30s
162-
163- - platform : uptime
164- name : Uptime Sensor
165- id : airgradient_basement_uptime
166193
167194font :
168195 # Font to use on the display
@@ -201,14 +228,20 @@ display:
201228 lambda : |-
202229 it.print(0, 0, id(font1), "HUM");
203230 it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f%%",id(humidity).state);
204- # - id: display_eco2
205- # lambda: |-
206- # it.print(0, 0, id(font1), "eCO2");
207- # it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(eco2).state);
208- # - id: display_tvoc
209- # lambda: |-
210- # it.print(0, 0, id(font1), "TVOC");
211- # it.printf(64, 24, id(font1), TextAlign::TOP_RIGHT, "%.0f",id(tvoc).state);
231+ - id : boot
232+ lambda : |-
233+ it.printf(0, 0, id(font1), TextAlign::TOP_RIGHT, "%s", get_mac_address().substr(6,11).c_str());
234+ on_page_change :
235+ to : boot
236+ then :
237+ - if :
238+ # Skip the boot page after initial boot
239+ condition :
240+ lambda : ' return id(device_uptime).state > 30;'
241+ then :
242+ - display.page.show_next : oled_display
243+ - component.update : oled_display
244+
212245
213246button :
214247 # https://github.com/esphome/issues/issues/2444
@@ -220,47 +253,65 @@ button:
220253 - senseair.background_calibration : senseair_s8
221254 - delay : 70s
222255 - senseair.background_calibration_result : senseair_s8
256+
223257 - platform : template
224258 name : SenseAir S8 Enable Automatic Calibration
225259 id : senseair_s8_enable_calibrate_button
226260 on_press :
227261 then :
228262 - senseair.abc_enable : senseair_s8
263+
229264 - platform : template
230265 name : SenseAir S8 Disable Automatic Calibration
231266 id : senseair_s8_disable_calibrate_button
232267 on_press :
233268 then :
234269 - senseair.abc_disable : senseair_s8
235270
236- http_request :
237- # Used to support POST request to send data to AirGradient
238- # https://esphome.io/components/http_request.html
239-
271+ - platform : template
272+ name : SenseAir S8 Show Calibration Interval
273+ id : senseair_s8_show_calibrate_interval
274+ on_press :
275+ then :
276+ - senseair.abc_get_period : senseair_s8
240277
241278interval :
242- - interval : 3s
243- # Cycle through page on display
279+ - interval : 5s
280+ # Automatically switch to the next page every five seconds
244281 then :
245- - display.page.show_next : oled_display
246- - component.update : oled_display
282+ - if :
283+ # Show boot screen for first 10 seconds with serial number and config version
284+ condition :
285+ lambda : ' return id(device_uptime).state < 10;'
286+ then :
287+ - display.page.show : boot
288+ - lambda : id(device_uptime).set_update_interval(1);
289+ else :
290+ # Change page on display
291+ - display.page.show_next : oled_display
292+ - component.update : oled_display
247293
248- - interval : 300s
294+ - interval : 2.5min
249295 # Send data to AirGradient API server
250296 then :
251- - http_request.post :
252- # AirGradient URL with the last 3 bytes of the MAC address in Hex format all lower case
253- url : !lambda |-
254- return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
255- headers :
256- Content-Type : application/json
257- # "!lambda return to_string(id(pm_2_5).state);" Converts sensor output from double to string
258- json :
259- wifi : !lambda return to_string(id(wifi_dbm).state);
260- pm01 : !lambda return to_string(id(pm_1_0).state);
261- pm02 : !lambda return to_string(id(pm_2_5).state);
262- pm10 : !lambda return to_string(id(pm_10_0).state);
263- pm003_count : !lambda return to_string(id(pm_0_3um).state);
264- rco2 : !lambda return to_string(id(co2).state);
265- atmp : !lambda return to_string(id(temp).state);
266- rhum : !lambda return to_string(id(humidity).state);
297+ if :
298+ condition :
299+ switch.is_on : upload_airgradient
300+ then :
301+ - http_request.post :
302+ # https://api.airgradient.com/public/docs/api/v1/
303+ # AirGradient URL with the MAC address all lower case
304+ url : !lambda |-
305+ return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address().substr(6,11) + "/measures";
306+ headers :
307+ Content-Type : application/json
308+ # "!lambda return to_string(id(pm_2_5).state);" Converts sensor output from double to string
309+ json :
310+ wifi : !lambda return to_string(id(wifi_dbm).state);
311+ pm01 : !lambda return to_string(id(pm_1_0).state);
312+ pm02 : !lambda return to_string(id(pm_2_5).state);
313+ pm10 : !lambda return to_string(id(pm_10_0).state);
314+ pm003_count : !lambda return to_string(id(pm_0_3um).state);
315+ rco2 : !lambda return to_string(id(co2).state);
316+ atmp : !lambda return to_string(id(temp).state);
317+ rhum : !lambda return to_string(id(humidity).state);
0 commit comments