Esphome with graphs

Edit: See a few posts further down (March 5, 2023) for an updated version that fixes all issues mentioned here.

I wrote an esphome configuration that displays historic data. You can find the yaml file below. Three issues, see also the comments in the file:

  1. It reboots every 18-24 hours
  2. On the ESP8266 I cannot also add mqtt and/or web_server. That is a pity and I’d love to find a solution.
  3. I have to manually turn off the display every evening. Does anyone know how to do that based on the time?

Contributions, questions, comments are most welcome! And many thanks to ajfriesen, who had shared an esphome file showing me how to read the air gradient sensors in the first place. :slight_smile:


# 15 Feb 2023, Argafål
#
# Features graphs to show the last 12 hours of measurements on the display.
# The display pages can be switched automatically every 5 seconds, or using a button.
#
# Known issues: 
# - The graphs require too much memory. On the ESP8266, adding either mqtt or
#   web_server to the configuration resulted in an Out of Memory Exception. Can
#   the number of data points be reduced (e.g. only one every 10 minutes)?
# - The device reports every ~18-24 hours. This might also be a memory issue.
#   How can I make sure to discard of data older than 12 hours, is that filling
#   up the memory?

# Helpful hints and suggestions for improvement are most appreciated. Email: airgradient@argafal.de, or use the AirGradient forum.

esphome:
  name: air-gradient
  platform: ESP8266
  board: d1_mini

  project:
    name: argafal.ESPHome-AirGradient
    version: "1.0"

# Enable logging
logger:

# Enable Home Assistant API
#api:

# https://github.com/esphome/issues/issues/3187
external_components:
  - source: github://pr#3384
    components: [ json ]

# If you have problem flashing the device over wifi, look at ajfriesen's solution here:
# https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1
ota:
  password: !secret ota_password

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  reboot_timeout: 15min

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Air-Gradient Fallback Hotspot"
    password: !secret fallback_ssid_password

captive_portal:

i2c:
  sda: D2
  scl: D1

uart:
  - rx_pin: D5
    tx_pin: D6
    baud_rate: 9600
    id: uart1
    
  - rx_pin: D4
    tx_pin: D3
    baud_rate: 9600
    id: uart2

font:
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_14
    size: 14
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_16
    size: 16
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_18
    size: 18
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_20
    size: 20
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_22
    size: 22
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_24
    size: 24

display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    contrast: 50%
    pages:
      - id: page1
        lambda: |-
          it.printf(0, 0, id(opensans_22), "%.1f°C  %.0f%%", id(temp).state, id(humidity).state);
          it.printf(0, 30, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);
      - id: page2
        # Blank page so that the button can be used to turn off the display at night. How can I do this based on time of day?
        lambda: |-
          it.printf(0, 0, id(opensans_22), " ");
      - id: page3
        lambda: |-
          it.graph(0, 0, id(single_temperature_graph));
          it.printf(0, 47, id(opensans_14), "Temp: %.1f°C", id(temp).state);
      - id: page4
        lambda: |-
          it.graph(0, 0, id(single_humidity_graph));
          it.printf(0, 47, id(opensans_14), "Humidity: %.0f%%", id(humidity).state);
      - id: page5
        lambda: |-
          it.graph(0, 0, id(single_co2_graph));
          it.printf(0, 47, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
      - id: page6
        lambda: |-
          it.graph(0, 0, id(multi_pm_graph));
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);


# Automatically switch to the next page every five seconds. Disabled, we now have a button
#interval:
#  - interval: 5s
#    then:
#      - display.page.show_next: oled
#      - component.update: oled

# Button
binary_sensor:
  - platform: gpio
    pin: D7
    name: "Button D7"
    filters:
      - delayed_on: 10ms
    on_press:
      - display.page.show_next: oled
      - component.update: oled

sensor:
  - platform: sht3xd
    temperature:
      id: temp
      name: "Temperature"
    humidity:
      id: humidity
      name: "Humidity"
    address: 0x44
    update_interval: 5s

  - platform: pmsx003
    # type can be PMSX003, PMS5003S, PMS5003T, PMS5003ST
    # https://esphome.io/components/sensor/pmsx003.html
    type: PMSX003
    uart_id: uart1
    pm_1_0:
      id: pm10
      name: "Particulate Matter <1.0µm Concentration"
    pm_2_5:
      id: pm25
      name: "Particulate Matter <2.5µm Concentration"
    pm_10_0:
      id: pm100
      name: "Particulate Matter <10.0µm Concentration"
# No hcho sensor in the air gradient package.
#    formaldehyde:
#      id: hcho
#      name: "Formaldehyde (HCHO) concentration in µg per cubic meter"

  - platform: senseair
    uart_id: uart2
    co2:
      id: co2
      name: "SenseAir CO2 Value"
    update_interval: 60s

# Not enough memory for MQTT and graphs together on ESP8266 :(
#mqtt:
#   broker: 192.168.1.XX
#   discovery: false

# Not enough memory for MQTT and graphs and web_server together on ESP8266 :(
#web_server:

graph:
  - id: single_temperature_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 1.0
    x_grid: 60min
    sensor: temp
  - id: single_humidity_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    sensor: humidity
  - id: single_co2_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 100.0
    x_grid: 60min
    sensor: co2
  - id: multi_pm_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    traces:
    - sensor: pm25
    - sensor: pm10
    - sensor: pm100

Using a Lolin C3 mini v2.1.0, I can run the graphs and mqtt output at the same time. See New Wemos board - #11 by argafal

For the reboot issue, I’ve found that you can either decrease the logging to INFO only, which appears to have permanently resolved the reboot issue with a PMS5003 and HomeAssistant for me, or you can increase the update_interval of the PMS sensor which will extend how long before it reboots, but not completely eliminate it

logger:
  level: INFO

Thanks to @MallocArray who painted out that I can remove the unused font definitions and who suggested to turn off logging.

I now can run MQTT at the same time as the graphs, on the ESP8266 that came with the kit. The reboot issue prevails but might be related to @ken830’s findings about two bugs in two different versions of SoftwareSerial.

The display turns off automatically now during night. Latest version attached for future reference:

# esphome on AirGradient Pro
# 28 Feb 2023, Argafål
#
# Features graphs to show the last 12 hours of measurements on the display.
# The display pages can be switched automatically every 5 seconds, or using a button.
#
# Known issues: 
# - The graphs require too much memory. On the ESP8266, adding either mqtt or
#   web_server to the configuration resulted in an Out of Memory Exception. Can
#   the number of data points be reduced (e.g. only one every 10 minutes)?
# - The device reboots every ~18-24 hours. This might also be a memory issue.
#   How can I make sure to discard of data older than 12 hours, is that filling
#   up the memory?
#
# Helpful hints and suggestions for improvement are most appreciated. Email:
# airgradient@argafal.de, or use the AirGradient forum.

esphome:
  name: air-gradient
  platform: ESP8266
  board: d1_mini

  project:
    name: argafal.ESPHome-AirGradient
    version: "1.0"

# Disable logging
logger:
  baud_rate: 0

# Home Assistant API
#api:

# https://github.com/esphome/issues/issues/3187
external_components:
  - source: github://pr#3384
    components: [ json ]

# If you have problem flashing the device over wifi, look at ajfriesen's solution here:
# https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1
ota:
  password: !secret ota_password

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  reboot_timeout: 15min

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Air-Gradient Fallback Hotspot"
    password: !secret fallback_ssid_password

captive_portal:

i2c:
  sda: D2
  scl: D1

uart:
  - rx_pin: D5
    tx_pin: D6
    baud_rate: 9600
    id: uart1
    
  - rx_pin: D4
    tx_pin: D3
    baud_rate: 9600
    id: uart2

font:
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_14
    size: 14
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_22
    size: 22

display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    contrast: 50%
    pages:
      - id: pageFront
        lambda: |-
          it.printf(0, 0, id(opensans_22), "%.1f°C  %.0f%%", id(temp).state, id(humidity).state);
          it.printf(0, 30, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);
      - id: pageDark
        # Blank page for night time
        lambda: |-
          it.printf(0, 0, id(opensans_22), " ");
      - id: page3
        lambda: |-
          it.graph(0, 0, id(single_temperature_graph));
          it.printf(0, 47, id(opensans_14), "Temp: %.1f°C", id(temp).state);
      - id: page4
        lambda: |-
          it.graph(0, 0, id(single_humidity_graph));
          it.printf(0, 47, id(opensans_14), "Humidity: %.0f%%", id(humidity).state);
      - id: page5
        lambda: |-
          it.graph(0, 0, id(single_co2_graph));
          it.printf(0, 47, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
      - id: page6
        lambda: |-
          it.graph(0, 0, id(multi_pm_graph));
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);


# Automatically switch to the next page every five seconds. Disabled, we now have a button
#interval:
#  - interval: 5s
#    then:
#      - display.page.show_next: oled
#      - component.update: oled

# Button
binary_sensor:
  - platform: gpio
    pin: D7
    name: "Button D7"
    filters:
      - delayed_on: 10ms
    on_press:
      - display.page.show_next: oled
      - component.update: oled

sensor:
  - platform: sht3xd
    temperature:
      id: temp
      name: "Temperature"
    humidity:
      id: humidity
      name: "Humidity"
    address: 0x44
    update_interval: 5s

  - platform: pmsx003
    # type can be PMSX003, PMS5003S, PMS5003T, PMS5003ST
    # https://esphome.io/components/sensor/pmsx003.html
    type: PMSX003
    uart_id: uart1
    update_interval: 30s
    pm_1_0:
      id: pm10
      name: "Particulate Matter <1.0µm Concentration"
    pm_2_5:
      id: pm25
      name: "Particulate Matter <2.5µm Concentration"
    pm_10_0:
      id: pm100
      name: "Particulate Matter <10.0µm Concentration"
# No hcho sensor in the air gradient package.
#    formaldehyde:
#      id: hcho
#      name: "Formaldehyde (HCHO) concentration in µg per cubic meter"

  - platform: senseair
    uart_id: uart2
    co2:
      id: co2
      name: "SenseAir CO2 Value"
    update_interval: 60s

#mqtt:
#   broker: 192.168.1.XX
#   discovery: false

# Not enough memory for graphs and web_server together on ESP8266 :(
#web_server:

graph:
  - id: single_temperature_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 1.0
    x_grid: 60min
    sensor: temp
  - id: single_humidity_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    sensor: humidity
  - id: single_co2_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 100.0
    x_grid: 60min
    sensor: co2
  - id: multi_pm_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    traces:
    - sensor: pm25
    - sensor: pm10
    - sensor: pm100


# Turn display off at night and on again in the morning
time:
   - platform: sntp
     timezone: Europe/Amsterdam
     id: esptime
     on_time:
       # Every evening
       - seconds: 0
         minutes: 30
         hours: 23
         days_of_week: MON-SUN
         then: 
              - display.page.show: pageDark
       # Every morning
       - seconds: 0
         minutes: 30
         hours: 8
         days_of_week: MON-SUN
         then: 
              - display.page.show: pageFront

Update:
I have now been running this for 66 hours without any reboot or problem. Here is the most recent version: the only changes are an additional uptime definition and the enabled mqtt. Works like a charm, so I updated the comment block in the beginning.

Thanks @MallocArray for your pointers.

If anyone decides to try my esphome definition, I’d love to read how it worked for you :slight_smile:

# esphome on AirGradient Pro
# 05 March 2023, Argafål
#
# Features:
# - The front page summarizes the current values.
# - The second page is a blank/dark page in order to disable the display.
# - The last 12 hours of measurements are displayed in graphs alongside their current value.
# - The display switches off automatically at night. 
# - The button (v3.7 of the AirGradient Pro board) flips through the
#   different pages/graphs.
# - For configurations without a button there is a commented code block that
#   shows how the display pages can be changed automatically every 5 seconds.
#
# Known issues:
# - None.
#
# Helpful hints and suggestions for improvement are most appreciated. Email:
# airgradient@argafal.de, or use the AirGradient forum.

esphome:
  name: air-gradient
  platform: ESP8266
  board: d1_mini

  project:
    name: argafal.ESPHome-AirGradient
    version: "1.0"

# Disable logging
logger:
  baud_rate: 0

# Home Assistant API
#api:

# https://github.com/esphome/issues/issues/3187
external_components:
  - source: github://pr#3384
    components: [ json ]

# If you have problem flashing the device over wifi, look at ajfriesen's solution here:
# https://community.home-assistant.io/t/esphome-flashing-over-wifi-does-not-work/357352/1
ota:
  password: !secret ota_password

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  reboot_timeout: 15min

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Air-Gradient Fallback Hotspot"
    password: !secret fallback_ssid_password

captive_portal:

i2c:
  sda: D2
  scl: D1

uart:
  - rx_pin: D5
    tx_pin: D6
    baud_rate: 9600
    id: uart1
    
  - rx_pin: D4
    tx_pin: D3
    baud_rate: 9600
    id: uart2

font:
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_14
    size: 14
  - file: "liberation_sans/LiberationSans-Regular.ttf"
    id: opensans_22
    size: 22

display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    contrast: 50%
    pages:
      - id: pageFront
        lambda: |-
          it.printf(0, 0, id(opensans_22), "%.1f°C  %.0f%%", id(temp).state, id(humidity).state);
          it.printf(0, 30, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);
      - id: pageDark
        # Blank page for night time
        lambda: |-
          it.printf(0, 0, id(opensans_22), " ");
      - id: page3
        lambda: |-
          it.graph(0, 0, id(single_temperature_graph));
          it.printf(0, 47, id(opensans_14), "Temp: %.1f°C", id(temp).state);
      - id: page4
        lambda: |-
          it.graph(0, 0, id(single_humidity_graph));
          it.printf(0, 47, id(opensans_14), "Humidity: %.0f%%", id(humidity).state);
      - id: page5
        lambda: |-
          it.graph(0, 0, id(single_co2_graph));
          it.printf(0, 47, id(opensans_14), "CO2: %.0f ppm", id(co2).state);
      - id: page6
        lambda: |-
          it.graph(0, 0, id(multi_pm_graph));
          it.printf(0, 47, id(opensans_14), "PM: %.0f %.0f %.0f ug/m3", id(pm10).state, id(pm25).state, id(pm100).state);


# Automatically switch to the next page every five seconds. Disabled, we now have a button
#interval:
#  - interval: 5s
#    then:
#      - display.page.show_next: oled
#      - component.update: oled

# Button
binary_sensor:
  - platform: gpio
    pin: D7
    name: "Button D7"
    filters:
      - delayed_on: 10ms
    on_press:
      - display.page.show_next: oled
      - component.update: oled

sensor:
  - platform: uptime
    name: "Uptime Sensor"
    id: uptime_raw
    update_interval: 1s

  - platform: sht3xd
    temperature:
      id: temp
      name: "Temperature"
    humidity:
      id: humidity
      name: "Humidity"
    address: 0x44
    update_interval: 5s

  - platform: pmsx003
    # type can be PMSX003, PMS5003S, PMS5003T, PMS5003ST
    # https://esphome.io/components/sensor/pmsx003.html
    type: PMSX003
    uart_id: uart1
    update_interval: 30s
    pm_1_0:
      id: pm10
      name: "Particulate Matter <1.0µm Concentration"
    pm_2_5:
      id: pm25
      name: "Particulate Matter <2.5µm Concentration"
    pm_10_0:
      id: pm100
      name: "Particulate Matter <10.0µm Concentration"
# No hcho sensor in the air gradient package.
#    formaldehyde:
#      id: hcho
#      name: "Formaldehyde (HCHO) concentration in µg per cubic meter"

  - platform: senseair
    uart_id: uart2
    co2:
      id: co2
      name: "SenseAir CO2 Value"
    update_interval: 60s

mqtt:
   broker: 192.168.1.42
   discovery: false
   topic_prefix: air-gradient

# Not enough memory for graphs and web_server together on ESP8266 :(
#web_server:

graph:
  - id: single_temperature_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 1.0
    x_grid: 60min
    sensor: temp
  - id: single_humidity_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    sensor: humidity
  - id: single_co2_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 100.0
    x_grid: 60min
    sensor: co2
  - id: multi_pm_graph
    duration: 12h
    width: 128
    height: 48 
    y_grid: 10.0
    x_grid: 60min
    traces:
    - sensor: pm25
    - sensor: pm10
    - sensor: pm100


# Turn display off at night and on again in the morning
time:
   - platform: sntp
     timezone: Europe/Amsterdam
     id: esptime
     on_time:
       # Every evening
       - seconds: 0
         minutes: 30
         hours: 23
         days_of_week: MON-SUN
         then: 
              - display.page.show: pageDark
       # Every morning
       - seconds: 0
         minutes: 30
         hours: 8
         days_of_week: MON-SUN
         then: 
              - display.page.show: pageFront