|
| 1 | +# AirGradient Open Air Outdoor Monitor with CO2 and TVOC |
| 2 | +# Model: O-1PST |
| 3 | +# https://www.airgradient.com/open-airgradient/instructions/overview/ |
| 4 | + |
| 5 | +# Needs ESPHome 2023.7.0 or later |
| 6 | + |
| 7 | +substitutions: |
| 8 | + devicename: "ag-open-air-o-1pst" |
| 9 | + upper_devicename: "AG Open Air CO2" |
| 10 | + ag_esphome_config_version: 0.1.0 |
| 11 | + |
| 12 | +esphome: |
| 13 | + name: "${devicename}" |
| 14 | + friendly_name: "${upper_devicename}" |
| 15 | + name_add_mac_suffix: true # Set to false if you don't want part of the MAC address in the name |
| 16 | + on_boot: |
| 17 | + priority: 200 # Network connections setup |
| 18 | + then: |
| 19 | + - http_request.post: |
| 20 | + # Return wifi signal -50 as soon as device boots to show activity on AirGradient Dashboard site |
| 21 | + # Using -50 instead of actual value as the wifi_signal sensor has not reported a value at this point in boot process |
| 22 | + url: !lambda |- |
| 23 | + return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures"; |
| 24 | + headers: |
| 25 | + Content-Type: application/json |
| 26 | + json: |
| 27 | + wifi: !lambda return to_string(-50); |
| 28 | + |
| 29 | +esp32: |
| 30 | + board: esp32-c3-devkitm-1 |
| 31 | + |
| 32 | +# Enable logging |
| 33 | +# https://esphome.io/components/logger.html |
| 34 | +logger: |
| 35 | + baud_rate: 0 # Must disable serial logging as it conflicts with pms5003 uart pins and ESP32-C3 only has 2 hardware UART |
| 36 | + # hardware_uart: USB_SERIAL_JTAG |
| 37 | + logs: |
| 38 | + component: ERROR # Hiding warning messages about component taking a long time https://github.com/esphome/issues/issues/4717 |
| 39 | + |
| 40 | +# Enable Home Assistant API |
| 41 | +api: |
| 42 | + |
| 43 | +ota: |
| 44 | + |
| 45 | +wifi: |
| 46 | + ssid: !secret wifi_ssid |
| 47 | + password: !secret wifi_password |
| 48 | + manual_ip: |
| 49 | + static_ip: 192.168.2.120 |
| 50 | + gateway: 192.168.2.1 |
| 51 | + subnet: 255.255.255.0 |
| 52 | + dns1: 192.168.2.1 |
| 53 | + dns2: 4.2.2.2 |
| 54 | + # Enable fallback hotspot (captive portal) in case wifi connection fails |
| 55 | + ap: |
| 56 | + |
| 57 | +# The captive portal is a fallback mechanism for when connecting to the configured WiFi fails. |
| 58 | +# https://esphome.io/components/captive_portal.html |
| 59 | +captive_portal: |
| 60 | + |
| 61 | +web_server: # Please note that enabling this component will take up a lot of memory and may decrease stability, especially on ESP8266. |
| 62 | + |
| 63 | +# Create a switch for safe_mode in order to flash the device |
| 64 | +# Solution from this thread: |
| 65 | +# https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1 |
| 66 | +switch: |
| 67 | + - platform: safe_mode |
| 68 | + name: "Flash Mode (Safe Mode)" |
| 69 | + icon: "mdi:cellphone-arrow-down" |
| 70 | + |
| 71 | + |
| 72 | +uart: |
| 73 | + # https://esphome.io/components/uart.html#uart |
| 74 | + - rx_pin: GPIO0 # Pin 12 |
| 75 | + tx_pin: GPIO1 # Pin 13 |
| 76 | + baud_rate: 9600 |
| 77 | + id: senseair_s8_uart |
| 78 | + |
| 79 | + - rx_pin: GPIO20 # Pin 30 or RX |
| 80 | + tx_pin: GPIO21 # Pin 31, or TX |
| 81 | + baud_rate: 9600 |
| 82 | + id: pms5003_uart |
| 83 | + |
| 84 | +i2c: |
| 85 | + # https://esphome.io/components/i2c.html |
| 86 | + sda: GPIO7 # Pin 21 |
| 87 | + scl: GPIO6 # Pin 20 |
| 88 | + frequency: 400kHz # 400kHz eliminates warnings about components taking a long time other than SGP40 component: https://github.com/esphome/issues/issues/4717 |
| 89 | + |
| 90 | +sensor: |
| 91 | + - platform: pmsx003 |
| 92 | + # PMS5003T with temperature and humidity https://esphome.io/components/sensor/pmsx003.html |
| 93 | + type: PMS5003T |
| 94 | + uart_id: pms5003_uart |
| 95 | + pm_2_5: |
| 96 | + name: "PM 2.5" |
| 97 | + id: pm_2_5 |
| 98 | + on_value: |
| 99 | + lambda: |- |
| 100 | + // https://en.wikipedia.org/wiki/Air_quality_index#Computing_the_AQI |
| 101 | + // Borrowed from https://github.com/kylemanna/sniffer/blob/master/esphome/sniffer_common.yaml |
| 102 | + if (id(pm_2_5).state <= 12.0) { |
| 103 | + // good |
| 104 | + id(pm_2_5_aqi).publish_state((50.0 - 0.0) / (12.0 - 0.0) * (id(pm_2_5).state - 0.0) + 0.0); |
| 105 | + } else if (id(pm_2_5).state <= 35.4) { |
| 106 | + // moderate |
| 107 | + id(pm_2_5_aqi).publish_state((100.0 - 51.0) / (35.4 - 12.1) * (id(pm_2_5).state - 12.1) + 51.0); |
| 108 | + } else if (id(pm_2_5).state <= 55.4) { |
| 109 | + // usg |
| 110 | + id(pm_2_5_aqi).publish_state((150.0 - 101.0) / (55.4 - 35.5) * (id(pm_2_5).state - 35.5) + 101.0); |
| 111 | + } else if (id(pm_2_5).state <= 150.4) { |
| 112 | + // unhealthy |
| 113 | + id(pm_2_5_aqi).publish_state((200.0 - 151.0) / (150.4 - 55.5) * (id(pm_2_5).state - 55.5) + 151.0); |
| 114 | + } else if (id(pm_2_5).state <= 250.4) { |
| 115 | + // very unhealthy |
| 116 | + id(pm_2_5_aqi).publish_state((300.0 - 201.0) / (250.4 - 150.5) * (id(pm_2_5).state - 150.5) + 201.0); |
| 117 | + } else if (id(pm_2_5).state <= 350.4) { |
| 118 | + // hazardous |
| 119 | + id(pm_2_5_aqi).publish_state((400.0 - 301.0) / (350.4 - 250.5) * (id(pm_2_5).state - 250.5) + 301.0); |
| 120 | + } else if (id(pm_2_5).state <= 500.4) { |
| 121 | + // hazardous 2 |
| 122 | + id(pm_2_5_aqi).publish_state((500.0 - 401.0) / (500.4 - 350.5) * (id(pm_2_5).state - 350.5) + 401.0); |
| 123 | + } else { |
| 124 | + id(pm_2_5_aqi).publish_state(500); |
| 125 | + } |
| 126 | + pm_1_0: |
| 127 | + name: "PM 1.0" |
| 128 | + id: pm_1_0 |
| 129 | + pm_10_0: |
| 130 | + name: "PM 10.0" |
| 131 | + id: pm_10_0 |
| 132 | + pm_0_3um: |
| 133 | + name: "PM 0.3" |
| 134 | + id: pm_0_3um |
| 135 | + temperature: |
| 136 | + name: "Temperature" |
| 137 | + id: temp |
| 138 | + humidity: |
| 139 | + name: "Humidity" |
| 140 | + id: humidity |
| 141 | + update_interval: 2min |
| 142 | + |
| 143 | + - platform: template |
| 144 | + name: "PM 2.5 AQI" |
| 145 | + unit_of_measurement: "AQI" |
| 146 | + icon: "mdi:air-filter" |
| 147 | + accuracy_decimals: 0 |
| 148 | + id: pm_2_5_aqi |
| 149 | + |
| 150 | + - platform: senseair |
| 151 | + # SenseAir S8 https://esphome.io/components/sensor/senseair.html |
| 152 | + # https://senseair.com/products/size-counts/s8-lp/ |
| 153 | + co2: |
| 154 | + name: "SenseAir S8 CO2" |
| 155 | + id: co2 |
| 156 | + filters: |
| 157 | + - skip_initial: 2 |
| 158 | + - clamp: |
| 159 | + min_value: 400 # 419 as of 2023-06 https://gml.noaa.gov/ccgg/trends/global.html |
| 160 | + id: senseair_s8 |
| 161 | + uart_id: senseair_s8_uart |
| 162 | + |
| 163 | + - platform: wifi_signal |
| 164 | + name: "WiFi Signal" |
| 165 | + id: wifi_dbm |
| 166 | + update_interval: 60s |
| 167 | + |
| 168 | + - platform: uptime |
| 169 | + name: "Uptime" |
| 170 | + id: device_uptime |
| 171 | + update_interval: 10s |
| 172 | + |
| 173 | + - platform: sgp4x |
| 174 | + # SGP41 https://esphome.io/components/sensor/sgp4x.html |
| 175 | + voc: |
| 176 | + name: "VOC Index" |
| 177 | + id: voc |
| 178 | + nox: |
| 179 | + name: "NOx Index" |
| 180 | + id: nox |
| 181 | + compensation: # Remove this block if no temp/humidity sensor present for compensation |
| 182 | + temperature_source: temp |
| 183 | + humidity_source: humidity |
| 184 | + |
| 185 | + |
| 186 | +button: |
| 187 | + # https://github.com/esphome/issues/issues/2444 |
| 188 | + - platform: template |
| 189 | + name: SenseAir S8 Calibration |
| 190 | + id: senseair_s8_calibrate_button |
| 191 | + on_press: |
| 192 | + then: |
| 193 | + - senseair.background_calibration: senseair_s8 |
| 194 | + - delay: 70s |
| 195 | + - senseair.background_calibration_result: senseair_s8 |
| 196 | + - platform: template |
| 197 | + name: SenseAir S8 Enable Automatic Calibration |
| 198 | + id: senseair_s8_enable_calibrate_button |
| 199 | + on_press: |
| 200 | + then: |
| 201 | + - senseair.abc_enable: senseair_s8 |
| 202 | + - platform: template |
| 203 | + name: SenseAir S8 Disable Automatic Calibration |
| 204 | + id: senseair_s8_disable_calibrate_button |
| 205 | + on_press: |
| 206 | + then: |
| 207 | + - senseair.abc_disable: senseair_s8 |
| 208 | + |
| 209 | +output: |
| 210 | + - platform: gpio |
| 211 | + # Watchdog to reboot if no activity |
| 212 | + id: watchdog |
| 213 | + pin: GPIO2 |
| 214 | + |
| 215 | +http_request: |
| 216 | + # Used to support POST request to send data to AirGradient |
| 217 | + # https://esphome.io/components/http_request.html |
| 218 | + |
| 219 | + |
| 220 | +interval: |
| 221 | + - interval: 30s |
| 222 | + # Notify watchdog device is still alive |
| 223 | + then: |
| 224 | + - output.turn_on: watchdog |
| 225 | + - delay: 20ms |
| 226 | + - output.turn_off: watchdog |
| 227 | + |
| 228 | + |
| 229 | + - interval: 2.5min |
| 230 | + # Send data to AirGradient API server |
| 231 | + then: |
| 232 | + - http_request.post: |
| 233 | + # https://api.airgradient.com/public/docs/api/v1/ |
| 234 | + # AirGradient URL with the MAC address all lower case |
| 235 | + url: !lambda |- |
| 236 | + return "http://hw.airgradient.com/sensors/airgradient:" + get_mac_address() + "/measures"; |
| 237 | + headers: |
| 238 | + Content-Type: application/json |
| 239 | + # "!lambda return to_string(id(pm_2_5).state);" Converts sensor output from double to string |
| 240 | + json: |
| 241 | + wifi: !lambda return to_string(id(wifi_dbm).state); |
| 242 | + pm01: !lambda return to_string(id(pm_1_0).state); |
| 243 | + pm02: !lambda return to_string(id(pm_2_5).state); |
| 244 | + pm10: !lambda return to_string(id(pm_10_0).state); |
| 245 | + pm003_count: !lambda return to_string(id(pm_0_3um).state); |
| 246 | + rco2: !lambda return to_string(id(co2).state); |
| 247 | + atmp: !lambda return to_string(id(temp).state); |
| 248 | + rhum: !lambda return to_string(id(humidity).state); |
| 249 | + tvoc_index: !lambda return to_string(id(voc).state); |
| 250 | + nox_index: !lambda return to_string(id(nox).state); |
0 commit comments