Skip to content

Instantly share code, notes, and snippets.

@aver-ua
Last active November 2, 2025 09:34
Show Gist options
  • Select an option

  • Save aver-ua/892d63ab0da7c6934eef24ec85b54e2f to your computer and use it in GitHub Desktop.

Select an option

Save aver-ua/892d63ab0da7c6934eef24ec85b54e2f to your computer and use it in GitHub Desktop.
ESPHome config for Air Quality Sensor (ENS160+AHT20)
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