Last active
November 2, 2025 09:34
-
-
Save aver-ua/892d63ab0da7c6934eef24ec85b54e2f to your computer and use it in GitHub Desktop.
ESPHome config for Air Quality Sensor (ENS160+AHT20)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| esphome: | |
| name: air-quality | |
| comment: "${device_description}" | |
| friendly_name: Air Quality | |
| substitutions: | |
| device_description: "Air Quality Sensor (ENS160+AHT20)" | |
| device_name: "air-quality" | |
| ens160_update_interval: "100s" | |
| aht20_update_interval: "60s" | |
| # when using a combo borad with ENS160+AHT20 there is an issue... | |
| # the heater of the ENS160 significantly warms up the AHT21 as there is an increase of about 6 °C when the ENS160 is producing data | |
| # so, there is a workaround: | |
| #temperature_offset: "-6.0" #"-6.5" | |
| #humidity_offset: "15" #"12.5" | |
| esp8266: | |
| board: d1_mini | |
| # Enable logging | |
| logger: | |
| # Enable Home Assistant API | |
| api: | |
| encryption: | |
| key: !secret api_encryption_key | |
| reboot_timeout: 0s | |
| ota: | |
| - platform: esphome | |
| password: !secret ota_password | |
| wifi: | |
| ssid: !secret wifi_ssid | |
| password: !secret wifi_password | |
| # Enable fallback hotspot (captive portal) in case wifi connection fails | |
| ap: | |
| ssid: "Air-Quality Fallback Hotspot" | |
| password: !secret ap_password | |
| captive_portal: | |
| # for reset_reason sensor | |
| debug: | |
| update_interval: 60s | |
| # Restart button | |
| button: | |
| - platform: restart | |
| name: Restart | |
| i2c: | |
| sda: D2 | |
| scl: D1 | |
| scan: true | |
| id: bus_a | |
| number: | |
| # Temperature Offset | |
| - platform: template | |
| id: temperature_offset | |
| name: Temperature Offset | |
| icon: mdi:plus-minus-variant | |
| optimistic: true | |
| mode: slider | |
| step: 1 | |
| entity_category: config | |
| min_value: -20 | |
| max_value: 20 | |
| initial_value: -6 | |
| restore_value: yes | |
| # Humidity Offset | |
| - platform: template | |
| id: humidity_offset | |
| name: Humidity Offset | |
| icon: mdi:plus-minus-variant | |
| optimistic: true | |
| mode: slider | |
| step: 1 | |
| entity_category: config | |
| min_value: -50 | |
| max_value: 50 | |
| initial_value: 15 | |
| restore_value: yes | |
| text_sensor: | |
| - platform: debug | |
| reset_reason: | |
| name: Reset Reason | |
| # Uptime Human Readable | |
| - platform: template | |
| name: Uptime HR | |
| id: uptime_human | |
| icon: mdi:clock-start | |
| entity_category: "diagnostic" | |
| # Air Quality Text | |
| - platform: template | |
| name: "Air Quality Rating" | |
| id: ens160_air_quality_rating | |
| icon: mdi:chart-areaspline | |
| # eCO2 Level Text | |
| - platform: template | |
| name: "eCO2 Rating" | |
| id: ens160_eco2_rating | |
| icon: mdi:chart-areaspline | |
| # TVOC Level Text | |
| - platform: template | |
| name: "TVOC Rating" | |
| id: ens160_tvoc_rating | |
| icon: mdi:chart-areaspline | |
| # binary sensor indicating active warning | |
| binary_sensor: | |
| - platform: template | |
| name: "Air Quality Warning" | |
| id: air_quality_warning | |
| icon: mdi:bell-alert | |
| - platform: template | |
| name: "CO2 Warning" | |
| id: co2_warning | |
| icon: mdi:bell-alert | |
| - platform: template | |
| name: "TVOC Warning" | |
| id: tvoc_warning | |
| icon: mdi:bell-alert | |
| sensor: | |
| - platform: ens160_i2c | |
| eco2: | |
| name: "ENS160 eCO2" | |
| id: ens160_eco2 | |
| on_raw_value: | |
| then: | |
| - text_sensor.template.publish: | |
| id: ens160_eco2_rating | |
| state: !lambda |- | |
| float co2 = (float)x; | |
| id(co2_warning).publish_state(co2>1000); | |
| if (co2<700) return {"Excellent"}; | |
| if ((co2>=700)&&(co2<900)) return {"Good"}; | |
| if ((co2>=900)&&(co2<1100)) return {"Fair"}; | |
| if ((co2>=1100)&&(co2<1600)) return {"Mediocre (Contaminated indoor air. Ventilation recommended)"}; | |
| if ((co2>=1600)&&(co2<2200)) return {"Bad (Heavily contaminated indoor air. Ventilation required)"}; | |
| if (co2>=2200) return {"Deadly dangerous!"}; | |
| return {"NA"}; | |
| tvoc: | |
| name: "ENS160 Total Volatile Organic Compounds" | |
| id: ens160_tvoc | |
| on_raw_value: | |
| then: | |
| - text_sensor.template.publish: | |
| id: ens160_tvoc_rating | |
| state: !lambda |- | |
| float tvoc = (float)x; | |
| id(tvoc_warning).publish_state(tvoc>700); | |
| if (tvoc<65) return {"Excellent"}; | |
| if ((tvoc>=65)&&(tvoc<220)) return {"Good. Ventilation recommended"}; | |
| if ((tvoc>=220)&&(tvoc<660)) return {"Moderate. Ventilation recommended"}; | |
| if ((tvoc>=660)&&(tvoc<2200)) return {"Poor. Ventilation required"}; | |
| if ((tvoc>=2200)&&(tvoc<5500)) return {"Unhealthy. Intensified entilation required"}; | |
| if (tvoc>=5500) return {"Deadly dangerous!"}; | |
| return {"NA"}; | |
| aqi: | |
| id: ens160_air_quality_index | |
| name: "ENS160 Air Quality Index" | |
| on_raw_value: | |
| then: | |
| - text_sensor.template.publish: | |
| id: ens160_air_quality_rating | |
| state: !lambda |- | |
| int q = (int)x; | |
| id(air_quality_warning).publish_state(q>3); | |
| switch ( q ) { | |
| case 1: return {"Excellent"}; | |
| case 2: return {"Good"}; | |
| case 3: return {"Moderate"}; | |
| case 4: return {"Poor"}; | |
| case 5: return {"Unhealthy"}; | |
| default: return {"NA"}; | |
| } | |
| update_interval: ${ens160_update_interval} | |
| address: 0x53 | |
| compensation: | |
| temperature: aht20_temperature | |
| humidity: aht20_humidity | |
| - platform: aht10 | |
| variant: AHT20 | |
| temperature: | |
| name: "AHT20 Temperature" | |
| id: aht20_temperature | |
| filters: | |
| #- offset: ${temperature_offset} | |
| - lambda: |- | |
| return x+id(temperature_offset).state; | |
| humidity: | |
| name: "AHT20 Humidity" | |
| id: aht20_humidity | |
| filters: | |
| #- offset: ${humidity_offset} | |
| - lambda: |- | |
| return x+id(humidity_offset).state; | |
| update_interval: ${aht20_update_interval} | |
| address: 0x38 | |
| # Uptime sensor. | |
| - platform: uptime | |
| name: Uptime | |
| entity_category: "diagnostic" | |
| id: esp_uptime | |
| update_interval: 60s | |
| on_raw_value: | |
| then: | |
| - text_sensor.template.publish: | |
| id: uptime_human | |
| state: !lambda |- | |
| int seconds = round(id(esp_uptime).raw_state); | |
| int days = seconds / (24 * 3600); | |
| seconds = seconds % (24 * 3600); | |
| int hours = seconds / 3600; | |
| seconds = seconds % 3600; | |
| int minutes = seconds / 60; | |
| seconds = seconds % 60; | |
| return ( | |
| (days ? to_string(days) + "d " : "") + | |
| (hours ? to_string(hours) + "h " : "") + | |
| (minutes ? to_string(minutes) + "m " : "") + | |
| (to_string(seconds) + "s") | |
| ).c_str(); | |
| # WiFi Signal sensor. | |
| - platform: wifi_signal | |
| name: "WiFi Signal dB" | |
| id: wifi_signal_db | |
| entity_category: "diagnostic" | |
| update_interval: 60s | |
| - platform: copy # Reports the WiFi signal strength in % | |
| source_id: wifi_signal_db | |
| name: "WiFi Signal Percent" | |
| filters: | |
| - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); | |
| unit_of_measurement: "%" | |
| entity_category: "diagnostic" | |
| device_class: "" | |
| # Enable Web server. | |
| web_server: | |
| port: 80 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment