Yet another ESP8266 w/ CSS811 / BME280 / OLED Display

Those little ESP8266 with 0.96" OLED are sometimes on Asian shops for just a few bucks, so I’ve chosen one of them for my 1st ESPHome project.

  • ESP8266 with 0.96" OLED-Display
  • BME280 Sensor (Temp/Humidity/Pressure)
  • CJMCU-811 Board (CSS811 Sensor with some setup to run the sensor on 3.3V)
  • base-board for ESP8266-dev-board
  • XH header, removed pins to fit them over the pins from the base-board
  • XH plugs and header 4pins/6pins for connecting both sensor boards

I’m using a CJMCU-811 (“purple 3.3V board” with connected WAK to GND and RST to 3V) and a BME280, to get the humidity and temperatur values correctly.
From the 2 6-pin headers on the base-board the pins are pulled out and the headers were positioned onto the pins from the base-board (would also work directly on the ESP8266 board directly).
The BME came with soldered in pins, did there the same trick with the XH header (pulled pins, stuck it over the existing pins).

Rated the TVOC value to the European AQI accordingly… (ok, its’s more a guess).

With the OLED display I was stretching a bit to get it work (address, where is SDA, SCL?) and then, after all, getting dimmed again during dusk/dawn/night time…

Maybe someone has a better idea with the font… I didn’t get it in the 1st attempt to load a ttf font from the config-dir and had issues with the gfont, not displaying unicode characters correctly. (Guess, this is because they are not being taken into the compiled code for the esp8266, have no clue, where to turn which screw, to fix that in general… HA is running on a RasPi with the standard installation)

TBD:

  • Index for the CO2-levels…
  • Text sensors for the HA frontend
  • Verifying the current baseline (Can this be automated? Is there a procedure to set up the baseline by “button press”? )
  • adjust the brightness according to +/- after sunrise / sunset instead of time
  • override the brightness with room lamp setting after sunset/before sunrise
# Build-in OLED-display 128x64 16 dots yellow (current top position) , 48 dots blue (current bottom position)

globals:
  - id: display_page
    type: int
    initial_value: '0'
  - id: ccs811_warmup
    type: bool
    initial_value: 'true'
  - id: oled_contrast
    type: float
    initial_value: '100'

# I2C Bus A für OLED Display
i2c:
  - id: bus_a
    scl: GPIO12  # D6
    sda: GPIO14  # D5
    frequency: 1000kHz  # tried to adopt to phone camera with 60Hz 
    scan: false  # deactivate scan on I2C-Bus; set active for unknown device adresses and set logging to verbose

# I2C Bus B for BME280
  - id: bus_b
    scl: GPIO2  # D4
    sda: GPIO0  # D3
    scan: false  

# I2C Bus C für CJMCU-811
  - id: bus_c
    scl: GPIO4  # D2
    sda: GPIO5  # D1
    scan: false  

sensor:
  - platform: bme280_i2c # this name changes resently from bme280 to bme280_i2c 
    i2c_id: bus_b
    temperature:
      name: "BME280 Temperature"
      id: temp_sensor
      oversampling: 16x
    pressure:
      name: "BME280 Pressure"
      id: pressure_sensor
    humidity:
      name: "BME280 Humidity"
      id: humidity_sensor
    address: 0x76
    update_interval: 60s

  - platform: ccs811
    i2c_id: bus_c
    eco2:
      name: "eCO2 Concentration"
      id: eco2_sensor
    tvoc:
      name: "TVOC Concentration"
      id: tvoc_sensor
    address: 0x5A
    #baseline: 0x1AB7 # 2024-07-01 -> skewed, persons were in the room
    baseline: 0x59B5 # 2024-07-03
    
    humidity: humidity_sensor  # humidity take from BME280-Sensor
    temperature: temp_sensor  # temperature taken form BME280-Sensor
    update_interval: 60s

interval:
  - interval: 3s
    then:
      - lambda: |-
          id(display_page) = (id(display_page) + 1) % 2;
          if (id(eco2_sensor).state == 400) {
            id(ccs811_warmup) = true;
          } else {
            id(ccs811_warmup) = false;
          }
          id(oled_096).update();

font:
  - file: "gfonts://Roboto"
    id: font_1
    size: 15
    # dont't know how to circumvent that a ttf-font doesn't load the needed characters "Umlaute" to work properly in German language, so I did this for workaround:
    glyphs: [
      A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,Ä,Ö,Ü,
      a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,ä,ö,ü,
      1,2,3,4,5,6,7,8,9,0,_,°,ß, 
      "\u0020", #space
      "\u0021", #!
      "\u0022", #"
      "\u0025", #%
      "\u0027", #'  
      "\u003A", #:
      "\u002E", #.
      "\u003B", #;
      "\u002C", #,
      "\u002D", #-
      "\u002B", #+
      "\u0028", #(
      "\u0029", #)
      "\u003C", #<
      "\u003E", #>
      ]

number: # slider for the frontend to override temporarily brightness
  - platform: template
    name: "Brightness OLED"
    id: brightness_oled
    unit_of_measurement: '%'
    mode: slider
    step: 1
    min_value: 0
    max_value: 100
    optimistic: true
    initial_value: 70
    on_value:
      then:  #on an oled display we have only 2 colors (on/off) so contrast works like brightness controll. Method set_brightness() seems to have no effect here.
        - lambda: |-
            id(oled_contrast) = x;
            id(oled_096).set_contrast(id(oled_contrast) / 100.0);

time: # automatic brightness adjustment according to the time
  - platform: sntp
    id: sntp_time
    timezone: Europe/Berlin
    on_time:
      - seconds: 0
        minutes: /1
      then:  #on an oled display we have only 2 colors (on/off) so contrast works like brightness controll. Method set_brightness() seems to have no effect here.
          - lambda: |-
              auto time = id(sntp_time).now();
              if (time.hour >= 6 && time.hour < 8) {
                id(oled_contrast) = 50;
              } else if (time.hour >= 8 && time.hour < 16) {
                id(oled_contrast) = 100;
              } else if (time.hour >= 16 && time.hour < 20) {
                id(oled_contrast) = 50;
              } else if (time.hour >= 20 && time.hour < 22) {
                id(oled_contrast) = 10;
              } else {
                id(oled_contrast) = 0;
              }
              id(brightness_oled).publish_state(id(oled_contrast));
              id(oled_096).set_contrast(id(oled_contrast) / 100.0);

display:
  - platform: ssd1306_i2c
    i2c_id: bus_a
    model: "SSD1306 128x64"
    address: 0x3C
    id: oled_096
    rotation: 180  # wanted to show label for the sensors on bottom in yellow
    update_interval: 3s  # Display-Update-Intervall
    lambda: |-
      float aqi_value = 0;
      std::string aqi_text = "Init";
      if (id(tvoc_sensor).has_state()) {
        float tvoc = id(tvoc_sensor).state;
        if (tvoc <= 65) {
          aqi_value = tvoc / 65 * 50;
          aqi_text = "gut";
        } else if (tvoc <= 220) {
          aqi_value = (tvoc - 65) / (220 - 65) * 50 + 50;
          aqi_text = "mäßig";
        } else if (tvoc <= 660) {
          aqi_value = (tvoc - 220) / (660 - 220) * 50 + 100;
          aqi_text = "eing. unges.";
        } else if (tvoc <= 2200) {
          aqi_value = (tvoc - 660) / (2200 - 660) * 100 + 150;
          aqi_text = "ungesund";
        } else if (tvoc <= 3000) {
          aqi_value = (tvoc - 2200) / (3000 - 2200) * 100 + 200;
          aqi_text = "sehr unges.";
        } else {
          aqi_value = 300;
          aqi_text = "gefährlich";
        }
      }
      it.fill(esphome::display::COLOR_OFF);
      if (id(display_page) == 0) {
        it.printf(0, 0, id(font_1), "Temp: %4.1f °C", id(temp_sensor).state);
        it.printf(0, 15, id(font_1), "Druck: %6.1f hPa", id(pressure_sensor).state);
        it.printf(0, 30, id(font_1), "Feuchte: %5.1f%%", id(humidity_sensor).state);
        it.printf(0, 45, id(font_1), "BME-280");
      } else {
        it.printf(0, 0, id(font_1), "eCO2: %4.0f ppm", id(eco2_sensor).state);
        it.printf(0, 15, id(font_1), "TVOC: %4.0f ppb", id(tvoc_sensor).state);
        it.printf(0, 30, id(font_1), "AQI: %3.0f (%s)", aqi_value, aqi_text.c_str());
        if (id(ccs811_warmup)) {
          it.printf(0, 45, id(font_1), "CSS-811 WarmUp");
        } else {
          it.printf(0, 45, id(font_1), "CSS-811");
        }
      }
1 Like