iot


CO2 sensor with LCD and ESP32-C3 Supermini  

I wanted to add an LCD screen to my CO2 sensor, so I bought a white LCD 1602 with an I2C controller. The I2C controller needs to be soldered to the LCD, but my basic soldering skills were sufficient for the task.

I also wanted to place it in a box, so I purchased this plastic enclosure but I cannot recommend it. It required a lot of glue from a glue gun to install the LCD and the ESP. I also had to use the soldering iron to create space for the ESP and a hole for the USB connector. I installed the plastic buttons but they are only decorative.

I made room for the sensors inside the box, but finally left them outside because they are more precise that way.

The ESP32-WROOM-32 was too large for the enclosure, so I used a ESP32-C3 Supermini with an expansion board. This is a really amazing board with a 32-bit RISC-V 160MHz microcontroller, WiFi, Bluetooh, I2C and UART. It’s not as powerful as the ESP32-WROOM-32 with a dual core 32-bit Xtensa 240Mhz, but it’s more than capable to control the sensors and the LCD.

This is the ESPHome configuration, which includes a switch to control the LCD backlight and a clock synchronizing the time with Home Assistant:

esphome:
  name: co2sensor
  friendly_name: CO2 Sensor

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

logger:
  level: ERROR

api:
  password: ""

ota:
  platform: esphome
  password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  reboot_timeout: 90s
  ap:
    ssid: ${friendly_name} Hotspot
    password: !secret wifi_password

captive_portal:

web_server:
  port: 80

debug:

time:
  - platform: homeassistant
    id: homeassistant_time
    timezone: "Europe/Madrid"

uart:
  rx_pin: GPIO20
  tx_pin: GPIO21
  baud_rate: 9600

i2c:
  sda: GPIO8
  scl: GPIO9

sensor:
  - platform: uptime
    name: Uptime
    filters:
      - lambda: return x / 60.0;
    unit_of_measurement: minutes

  - platform: wifi_signal
    name: "WiFi signal sensor"
    update_interval: 60s

  - platform: dht
    model: AM2302
    pin: GPIO10
    temperature:
      id: "dht22_temperature"
      name: "DHT22 Temperature"
    humidity:
      id: "dht22_humidity"
      name: "DHT22 Humidity"
    update_interval: 50s

  - platform: mhz19
    co2:
      id: "mhz19_co2"
      name: "MH-Z19 CO2"
    temperature:
      id: "mhz19_temperature"
      name: "MH-Z19 Temperature"
    update_interval: 60s
    automatic_baseline_calibration: false

display:
  - platform: lcd_pcf8574
    dimensions: 16x2
    address: 0x27
    update_interval: 1s
    lambda: |-
        auto time =  id(homeassistant_time).now();
        it.printf(11, 0, "%02d:%02d", time.hour, time.minute);
        it.printf(11, 1, "%02d/%02d", time.day_of_month, time.month);
        auto co2 = id(mhz19_co2).state;
        if (!isnan(co2)) {
          it.printf(0, 0, "%.0fppm", co2);
        }
        it.printf(0, 1, "%.1fC", id(dht22_temperature).state);
        it.printf(6, 1, "%.0f%%", id(dht22_humidity).state);

switch:
  - platform: gpio
    pin: GPIO7
    name: "LED Backlight"
    id: led_backlight
    restore_mode: ALWAYS_OFF

And the full BOM:


MH-Z19B CO2 sensor  

To keep a healthy environment at home or at the workplace, one of the important things to control is the carbon dioxide (CO2) level.

It’s measured in ppm (parts per million), indicating how many parts of CO2 there are in one million parts of air. As a reference:

  • Less than 1000 ppm are healthy levels
  • Between 1000 ppm and 2000 ppm, we need to reduce the CO2 levels
  • Levels greater than 2000 ppm are associated with headaches, sleepiness, poor concentration, loss of attention…

To reduce the CO2 level, we need to ventilate the room. It can be manually done (opening the windows) or it can be automated with a ventilation system.

To measure it we need a proper CO2 sensor, and one of the most reliables sensors is the MH-Z19B. It is not cheap for the Aliexpress standards (it costs around 20 EUR), but other cheap sensors announced as “air quality” sensors or “eCO2” sensors are not really measuring the CO2 level (i.e. the MQ135).

I bought this MH-Z19B from Aliexpress and hooked it to an ESP32-WROOM-32 board. This board is going to be also purposed as a temperature and humidity sensor, so I also attached a DHT22 sensor. I bought this DHT22 sensor but it is not an original one, and the measures do not seem very correct, so I ordered again an original AM2302 (=DHT22). The MH-Z19B includes a temperature sensor, but it’s mainly used for calibration and it lacks precision, as it does not report decimals. I’m also using an expansion board to simplify the connections.

The ESP32-WROOM-32 on the expansion board connected to the MH-Z19B (gold) and to the DHT22 (red)
  • The jumper in the expansion board needs to be set to 5V (because both of these sensors need 5V).
  • Connected VCC and GND of both sensors to the expansion board
  • Connected the RX and TX of the MH-Z19B to the TX and RX (GPIO1 and GPIO3) of the ESP
  • Connected GPIO16 to the DAT of the DHT22

Finally, I installed ESPHome to the board with this configuration:

substitutions:
devicename: co2sensor
friendly_name: CO2 sensor

esphome:
name: ${devicename}
friendly_name: ${friendly_name}
platform: ESP32
board: nodemcu-32s

logger:

api:
password: ""

ota:
password: ""

wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
reboot_timeout: 90s
ap:
ssid: ${friendly_name} Hotspot
password: !secret wifi_password

captive_portal:

web_server:
port: 80

debug:

time:
- platform: homeassistant
id: homeassistant_time

uart:
rx_pin: GPIO3
tx_pin: GPIO1
baud_rate: 9600

sensor:
- platform: uptime
name: Uptime
filters:
- lambda: return x / 60.0;
unit_of_measurement: minutes

- platform: wifi_signal
name: "WiFi signal sensor"
update_interval: 60s

- platform: dht
model: DHT22
pin: GPIO16
temperature:
id: "dht22_temperature"
name: "DHT22 Temperature"
humidity:
id: "dht22_humidity"
name: "DHT22 Humidity"
update_interval: 60s

- platform: mhz19
co2:
id: "mhz19_co2"
name: "MH-Z19 CO2"
temperature:
id: "mhz19_temperature"
name: "MH-Z19 Temperature"
update_interval: 60s
automatic_baseline_calibration: false

And It’s working nicely, this is the ESPHome web interface:

Keeping the CO2 levels under control is helping me create a healthier workplace environment and improve performance.


Improving WiFi reception with an ESP32-WROOM-32U

I’m using a ESP32 with ESPHome connected to my heating system for climate control, as expained in a previous post.

The heating system is in a different builng than the router and I was experiencing some WiFi coverage issues (the WiFi signal needs to cross two metallic window blinds…).

To diagnose the WiFi coverage is very useful the wifi_signal sensor in ESPHome:

sensor:
  - platform: wifi_signal
    name: Wifi Signal
    update_interval: 60s

It was showing a WiFi signal of -95 dBm in the board: This is very low, and it was experiencing some disconnections.

Usually the ESP32 boards have an antenna integrated in the board, but the ESP32-WROOM-32U has an IPEX connector for an external antenna:

So, I spent less than 10 EUR in Aliexpress buying (affiliate links):

And replaced the previous ESP32-WROOM-32 module with an ESP32-WROOM-32U, installing the external antenna. This is how it looks now:

ESP32-WROOM-32U mounted on an expasion board connected to the 4 relay module

The WiFi signal shown in ESPHome increased from -95 dBm to –75 dBm and it’s no longer experiencing any interruptions.


ESPHome in a RTL8710BX smart plug

I bought from a popular chinese store a generic Tuya smart plug with power monitoring. It was extrememly cheap, costing less than 4 EUR. And of course I bought it to play trying to flash ESPHome.

The first challenge was to open it without breaking it. I was able to open it by wrapping it in cardboard and gently tapping it with a hammer around the body.

You never know what chip you are going to find. In the past ESP8266 was very common but now they switched mainly to Beken chips. This smart switch has a T102_V1.1 board with a Realtek RTL8710BX chip:

Luckily the support for this chip was developed in the LibreTiny project:

https://github.com/libretiny-eu/libretiny

And now it’s integrated into ESPHome:

https://esphome.io/components/libretiny

This is the board:

https://fcc.report/FCC-ID/2AU7O-T102V11/4540736.pdf

And after investigating the outputs, I reached this conclusion about them:

IndexRTL8710BXConnection
1VDDConnected
3GNDConnected
5GPIO_A18/UART0_RXDConnected to the button
7GPIO_A23/UART0_TXDNot connected
9GPIO_A14/PWM0 Power Monitor SEL pin
11GPIO_A15/PWM1Connected to the relay
2 GPIO_A12/PWM3Power monitor CF1 pin
4GPIO_A0/PWM2Power monitor CF pin
6GPIO_A5/PWM4Status LED inverted (there is another LED connected to the relay)
8GPIO_A30/DEBUG_LOG_TXNot connected, I soldered a cable to the flasher RX
10GPIO_A29/DEBUG_LOG_RXNot connected, I soldered a cable to the flasher TX

ESPHome

I created one device this config in the ESPHome dashboard (without power monitoring, read below if you want to enable it):

substitutions:
  devicename: smartplug1
  friendly_name: Smart Plug 1

esphome:
  name: ${devicename}
  friendly_name: ${friendly_name}

rtl87xx:
  board: wr2
  framework:
    version: 1.5.1

logger:

api:
  password: ""

ota:
  password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: ${friendly_name} Fallback Hotspot
    password: !secret wifi_password

captive_portal:

web_server:
  port: 80

status_led:
  pin:
    number: PA5
    inverted: true

text_sensor:
  - platform: libretiny
    version:
      name: LibreTiny Version

sensor:
  - platform: uptime
    name: Uptime
    filters:
      - lambda: return x / 60.0;
    unit_of_measurement: minutes

  - platform: wifi_signal
    name: Wifi Signal
    update_interval: 60s

binary_sensor:
  - platform: gpio
    device_class: power
    name: Button
    pin:
      number: PA18
      mode: INPUT_PULLUP
      inverted: true
    on_press:
      - switch.toggle: relay

switch:
  - platform: gpio
    id: relay
    name: ${friendly_name}
    pin: PA15
    restore_mode: RESTORE_DEFAULT_OFF

There is currently an open bug in LibreTiny and with the board t102-v1.1 the PA15 output does not work, so I needed to use the board wr2 on line 10.

I built it from the ESPHome dashboard and downloaded a .uf2 file.

Connecting an USB UART to the chip

I soldered four dupont cables to VDD, GND, GPIO_A29 and GPIO_A30 connecting them to an FTDI232 USB UART:

While connected to the FTDI232, the log from the chip can be viewed, i.e. with minicom (but remember to disable hardware flow control):

minicom -D /dev/ttyUSB0 -b 115200

Flashing

The official flashing guide does not recomment to power the chip with the USB flasher, but it worked for me:

https://docs.libretiny.eu/docs/platform/realtek-ambz/#flashing

You need the ltchiptool tool, I installed it in a Python virtualenv:

python3 -m venv .
source bin/activate
pip install ltchiptool zeroconf

The ltchiptool GUI caused some segmentation fault, so I used it from the commmand line.

To put the chip in flash mode, we need to power it with the TX pin connected to GND.

In flash mode, I created a backup of the previous firmware:

ltchiptool flash read realtek-ambz flash-backup.bin

And to write the ESPHome firmware:

ltchiptool flash write smartplug1.uf2


After the flashing, if I try to power it from USB the WiFi module did not start and it causes a boot loop, but It worked perfectly plugging it into the mains power. A new device appeared in the router and I can connect to the ESPHome web dashboard.

Adding power metering

The plug includes a power metering chip: the BL0936, that is supported by ESPHome:

https://esphome.io/components/sensor/hlw8012.html

However, after configuring and uploading the firmware with the power meter enabled to the board, the device enters a boot loop, displaying the following error:

[D][switch:016]: 'Smart Plug 1' Turning OFF.
[D][binary_sensor:034]: 'Button': Sending initial state OFF
[C][hlw8012:014]: Setting up HLW8012...
W [      0.109] CHANGE interrupts not supported

Luckily, after 10 reboots, the firmware enters in the “OTA safe mode”, disabling all the modules and connecting to the WiFi without the web dashboard but opening a port to allow remote flashing.

https://esphome.io/components/ota.html

It is a problem in the combination of the RTL8710 chip and the BL0936 module, there is an open issue about this:

https://github.com/libretiny-eu/libretiny/issues/155

It can be fixed with the workaround of SuperXL2023 modifying the .esphome/platformio/platforms/libretiny/cores/realtek-amb/arduino/src/wiring_irq.c file and adding the lines 64 and 65:

62: #if LT_RTL8720C
63:                        event = IRQ_FALL_RISE;
64: #elif LT_RTL8710B
65:                        event = IRQ_RISE;
66: #else
67:                        LT_W("CHANGE interrupts not supported !!!!!!");

In the ESPHome config I’m specifying the version of the framework to avoid losing this fix in an automatic update. It works perfectly after rebuilding the image with this fix and uploading it to the device.

This is the complete ESPHome configuration with power metering:

substitutions:
  devicename: smartplug1
  friendly_name: Smart Plug 1

  voltage_divider: "1400"
  current_resistor: "0.001"
  current_multiply: "1.0"

esphome:
  name: ${devicename}
  friendly_name: ${friendly_name}

rtl87xx:
  board: wr2
  framework:
    version: 1.5.1

logger:

api:
  password: ""

ota:
  password: ""

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  ap:
    ssid: ${friendly_name} Fallback Hotspot
    password: !secret wifi_password

captive_portal:

web_server:
  port: 80

status_led:
  pin:
    number: PA5
    inverted: true

text_sensor:
  - platform: libretiny
    version:
      name: LibreTiny Version

sensor:
  - platform: uptime
    name: Uptime
    filters:
      - lambda: return x / 60.0;
    unit_of_measurement: minutes

  - platform: wifi_signal
    name: Wifi Signal
    update_interval: 60s

  - platform: hlw8012
    model: BL0937
    sel_pin:
      number: PA14
      inverted: true
    cf_pin:
      number: PA0
    cf1_pin:
      number: PA12

    current:
      name: Current
      filters:
        - multiply: ${current_multiply}
    voltage:
      name: Voltage
    power:
      name: Power
    energy:
      name: Energy

    update_interval: 30s
    current_resistor: ${current_resistor}
    voltage_divider: ${voltage_divider}

binary_sensor:
  - platform: gpio
    device_class: power
    name: Button
    pin:
      number: PA18
      mode: INPUT_PULLUP
      inverted: true
    on_press:
      - switch.toggle: relay

switch:
  - platform: gpio
    id: relay
    name: ${friendly_name}
    pin: PA15
    restore_mode: RESTORE_DEFAULT_OFF

It needs to calibrate the sensor to obtain the proper values of voltage_divider, current_resistor and current_multiply. It can be done with a multimeter and entering the values in the hlw8012 page.


Orange Pi 3B

I’ve never been a fan of the Raspberry Pi. In my opinion, it occupies an intermediate position where it is too underpowered for desktop use and too overpowered for IoT projects:

  • To use them as a desktop, there are great X86 alternatives available at about the same price than a RPi 5 but much more powerful, such as the Intel N100.
  • And for IoT projects, the ESP32 is the king, with amazing boards with Wifi, Bluetooth, etc., all at a price of less than 5 euros.

So it’s place may be TV boxes (where I prefer a Chromecast with Android) or small servers where the power consumtion is important because they are always on.

I bought an Orange Pi 3B: 4 cores, 4GB RAM, 64GB eMMC (~50 euros in Aliexpress) to replace my old X86 home server (Intel N450: 2 cores, 2 GB RAM, 64GB SSD):

http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-3B.html

The Orange Pi 3B shares the form factor with the Raspberry Pi 3B but it is almost as powerful as the Raspberry Pi 4. Notably, the Orange Pi 3B comes with several advantages over the RPi 4:

  • Support for eMMC (much faster and reliable than SD cards)
  • A power button
  • A full-size HDMI port
  • External antenna
  • And it’s cheaper

I installed the Ubuntu Jammy server image in the eMMC following the OPi manual. It needs to use a USB-A male to USB-A male cable and the RKDevTool (it’s in Chinese) that runs only in Windows.

And, as this machine is going to be exposed to internet, I hardened a bit the security:

  • Changed the APT repositories to ports.ubuntu.com
  • Regenerated SSH server keys
  • Removed SSH root access
  • Changed passwords
  • Renamed the orangepi user
  • Removed the local autologin

To remove the local autologin we need to edit:

  • /lib/systemd/system/getty@.service.d/override.conf: For the display console autologin
  • /lib/systemd/system/serial-getty@.service.d/override.conf: For the serial console autologin
[Service]
ExecStartPre=/bin/sh -c 'exec /bin/sleep 10'
ExecStart=
ExecStart=-/sbin/agetty --noissue --autologin orangepi %I $TERM
Type=idle

Removing the “–autologin orangepi”. If you rename the orangepiuser but you want to keep the autologin, you’ll also need to change the username here.

Then I moved the docker containers and other services from my old X86 server:

  • Home Assistant (docker container)
  • ESPHome dashboard (docker container)
  • Pi-hole (docker container)
  • nginx (for certbot and DNS DoT for Pihole)
  • certbot (to maintain the SSL certificate for Home Assistant)
  • ddclient (dynamic DNS updater)
  • NAS (do not expect anything fancy, I access a USB disk via SSH, it’s enough for Kodi & backups)

Everything seems to work smoothly now.