HVAC - VAV control via ESPHome

I have ducted heating & cooling in our house, with eight Variable Air Volume (VAV) control dampers that manage the flow of air to various rooms in my house. They use a system from Advantage Air to control them (on or off - so calling them variable is a bit of a stretch… I guess you could time how long it takes to open/close and use that to work out percentage open/close but frankly that sounds like a lot of work for little real benefit). Basically there are two central hubs that look like ethernet switches, and from each of them run four RJ12 cables (one to each VAV), and four RJ45 cables (one for each individual wall controller that are used to open/close the VAV either manually or depending on a set temperature).


The central hubs are linked to each other via an RJ45 cable, and each have two power supplies - one 12V DC to power the hub and the four wall controllers, and one 24V AC supply to power the four VAVs.

Anyhow, some of the VAVs were not working reliably. I eventually diagnosed a faulty dc power supply (causing some of the panels to fail) and a faulty AC supply (preventing the VAVs connected to the central hub with the faulty supply from opening or closing). In both cases the fault was intermittent - one of my (not) favourite things as it can be, and was, a beast to diagnose.

I replaced the power supplies, but found that one of the central hubs had probably been fried by the old faulty 24VAC supply. Due to age, a replacement hub seemed to be impossible to source. Maybe a second hand unit could be found, but no guarantee how long it would last. By the way, 24VAC is safe for a non electrician to play with. I would hesitate to put my tongue on the bare wires, but otherwise nothing to be scared of. Be aware that some VAVs use higher mains voltages, in which case get an electrician.

What to do. Well. I had a spare Wemos D1 R2 so went out and got a 4 relay module and some RJ12 PCB sockets, plus found a 12v power supply (for the relay module) that also had a usb socket (for the wemos). Note that you can buy the parts cheaper from the likes of Aliexpress, including RJ12 sockets that are already wired, if you are willing to wait. In my case I used bits from my parts bin, from the local parts store, and next day delivery from Amazon.

At this point I should note that there is a limit to the number of GPIO ports you can reliably use (some can cause issues with booting depending on what’s connected) on an ESP2866 type device (and how many that are available depends on the ESP8266 device as well) before needing to use an external board that can cost as much as or more than just getting another ESP8266, so although I eventually will want to control eight VAVs and I just got a four relay module, an eight relay module would have been problematic and in any case currently I only needed to control four. There are other options out there such as an esp32 that will by default provide many more GPIO connections, but that’s another topic entirely. I am treating this as a Proof of Concept anyhow as I have also ordered a 4 relay zigbee module that is 24VAC powered that might be able to be used instead, but will see when it eventually arrives from Aliexpress! If the zigbee device doesn’t work [update - it didn’t. I suspect it doesn’t like my old zigbee gateway, so will wait until I get around to updating that], then I’ll tidy up what I’ve put together - maybe even make up a proper PCB and case and duplicate it. If the zigbee device works, then I’ll probably get another and return my wemos etc to the parts bin. Further update - the zigbee relay did work, kinda, once I put in a new gateway but I found one of the relays would not fire reliably. So it was a nice idea, but that was a fail. Esp32 it is.

On to the build. For my VAVs I found that the RJ12 cable running to it had four wires. The two central wires were ‘common’ and always connected to the 24VAC supply. The other two wires opened or closed the VAV depending on which one had 24VAC running through it.

So, soldered four wires to each RJ12 socket (actually, being an idiot I initially used four RJ11 sockets which were too small - I was tricked by the fact the VAVs only used 4 wires so assumed they were RJ11 but no, RJ12, sigh) and joined the two central wires from each socket together and that to one side of the ac supply. I daisy chain connected the common of each relay together and then to the other side of the AC supply.


[The photo is of the second relay board I setup after discovering the zigbee device was a no go - this second one I directly soldered to a wemos D1 mini rather than use Dupont cables]

Then I connected the outer wires of each RJ12 socket to the outer terminals of each relay. I kept a constant colour scheme for the cabling so I used NO to open the VAV and NC to close. Not a biggie as can always adjust that in the code but why not plan in advance. I also had a spare DHT temp/humidity sensor and some spare GPIOs left so I hooked that up as well so I could now monitor the temperature and humidity in the roof because, umm, well why the heck not. :wink:


I put the mess into a plastic container for some protection. I’m not proud, but hey it’s better than nothing and is good enough for a proof of concept. Of course the temp readings are higher than ambient as it’s in the same box, so if I don’t replace the whole thing later then I’ll run that outside.

Then I added the wemos into Home Assistant via Esphome and gave it a simple config:

esphome:
  name: hvac01
  friendly_name: VAV duct relay
  platform: ESP8266
  board: d1_mini

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

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: randomcodekjfhasdfklahdlfakjhds=

ota:
  password: "no this is not the actual password"

sensor:
  - platform: dht
    model: AM2302
    pin: D1
    temperature:
      name: "hvac01 Temp"
    humidity:
      name: "hvac01 Humidity"
    update_interval: 60s
  - platform: uptime
    name: "hvac01 Uptime"
  - platform: wifi_signal
    name: "hvac01 WiFi Signal"
    update_interval: 60s

switch:
  - platform: gpio
    pin:
      number: D2
    id: relay1
    name: "VAV01"
    icon: "mdi:hvac"
  - platform: gpio
    pin:
      number: D5
    id: relay2
    name: "VAV02"
    icon: "mdi:hvac"
  - platform: gpio
    pin:
      number: D6
    id: relay3
    name: "VAV03"
    icon: "mdi:hvac"
  - platform: gpio
    pin:
      number: D7
    id: relay4
    name: "VAV04"
    icon: "mdi:hvac"

Done. The old wall controllers are currently useless, but as each room has a separate Xiaomi temperature sensor on the wall I should be able to setup an automation now - well, when I can get around to it - to open and close the ducts to maintain a constant temperature in the house. I might think about reverse engineering the wall control panels, but it might be easier (and provide more features) to simply replace them as well. A future project. Already have an ethernet cable there so could potentially use a POE wall panel - although they are super expensive - but might simply use old tablets instead and use the RJ45 cable to just run power.

For those still playing at home, I replaced my wemos d1 with an esp32 with more GPIO pins so could control all eight valves. I am also working on a wall controller using an e-ink display - have put together a test system which works but am currently waiting for some more esp32s to arrive as I want more GPIO pins to support additional buttons and sensors. I will update here once I have it in a better state. In the meantime I noticed I mentioned in passing that you could easily reverse the VAV code from NO to NC if you got the wires crossed so when you open a valve it actually closes - FWIW it’s just adding in “inverted” in the code:

# Games room
  - platform: gpio
    pin:
      number: GPIO25
    id: relay8
    name: "VAV08"
    icon: "mdi:hvac"

to

# Games room
  - platform: gpio
    pin:
      number: GPIO25
    inverted: yes
    id: relay8
    name: "VAV08"
    icon: "mdi:hvac"

…and a further update. I now have my controller in what I would call a ‘beta’ state - the case I’ve made is a little rough but not too bad. Currently I am using some dupont connectors, but once I complete the final design I will likely go to directly soldering things to enable the case to be made a lot slimmer. I might also change a few other things such as smaller buttons just to make it all a little neater, and magnets to clip the case together.


Some notes:

  • I had to change to a 30 pin ESP32 so I could have enough GPIOs for sensors and buttons. This is slightly annoying as it’s twice the size of my preferred Wemos D1 mini, but it’s still not too bad.
  • I found that the ESP32 was more sensitive with my wifi network - partly due to power supply issues but mainly because it was having issues because the signal was too strong - but I was able to fix that with some tweaks to the code.
  • I did try using a Lilygo e-paper screen, but I preferred the Waveshare screen - it just seemed to have better contrast
  • I also tried a colour LCD panel from Lilygo - it was good and as a bonus had integrated buttons and ESP32, but I found it to be too distracting in particular on bedroom walls where these are mainly intended to go. I could use a motion sensor to turn it on/off but I ended up just preferring the e-ink screen. As an aside, I do have an older iPad on a wall in the living area running the HA app in kiosk mode and it works quite well - I had to use a motion sensor via homekit to wake it up but that’s another story.
  • I also thought about a touch screen, but I didn’t want a big panel on the wall and small touch screens can be problematic to use
  • I settled on a four button design. One for reset, one to cycle through multiple screens, and two buttons to perform actions on the currently active screen (eg change the thermostat temperature, mode, or open/close the duct)
  • My old controllers that were wired in using cat5 cable, so I was able to used a passive PoE injector to provide 5volts to the ESP32 - so no visible cabling. I have calculated that my supply should be able to support at least five controllers, but I guess I’ll see!
  • I’ve used a DS18B20 on a board to try to provide fairly accurate temperature readings, mounting it away from any heat sources, pointing it at a vent and screwed down using a M2 brass nut just to be fancy, but hot glue would be fine of course.

Esphome code:

substitutions:
  devicename: wall04
  friendname: Wall Control 04
  location: master
# ESP32 Wroom DevKit
  board: esp32dev
# Display pins
# Colour scheme based on cable provided by Waveshare
# Taking advantage of extra pins in 30 pin ESPWroom 32 DevKit board
# Avoiding GPIO pins that could cause issues
  clpin: GPIO16 # Yellow
  mopin: GPIO17 # Blue
  cspin: GPIO18 # Orange
  dcpin: GPIO19 # Green
  bupin: GPIO21 # Purple
  repin: GPIO22 # White
# Sensor pins
  dapin: GPIO5 # For Dallas temp sensor
# Button pins
  b1pin: GPIO25
  b2pin: GPIO26
  b3pin: GPIO27
# Reset button is the EN ie Enable pin

esphome:
  name: $devicename
  friendly_name: $friendname
 # on_boot:
 #   priority: 800
 #   then:
 #     - display.page.show: boot0
 #     - component.update: mydisplay
 #     - delay: 1s
 #     - wait_until:
 #         wifi.connected:
 #     - display.page.show: boot1
 #     - component.update: mydisplay
 #     - wait_until:
 #         api.connected:
 #     - display.page.show: boot2
 #     - component.update: mydisplay

esp32:
  board: $board
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "putapikeyhere"

ota:
  password: !secret ota_password

wifi:
# Added these due to some weird issues with connections    
  fast_connect: true
  reboot_timeout: 5min
  output_power: 15dB
# End bodgey stuff - note that this can only use one network so backup SSID no go

  manual_ip:
    static_ip: 192.168.0.44
    gateway: 192.168.0.1
    subnet: 255.255.255.0
# Note that if having issues connecting, may need to try fixed IP but also check power supply
# I had issues - assumed problem was 2.4ghz and 5ghz clash
# Eventually worked out ESP32 not getting enough power from weedy supply AND needed to set fast connect
# Keep in mind powering screen and any sensors
  networks:
  - ssid: !secret wifIoT_ssid
    password: !secret wifIoT_password
#    priority: 2

# Backup SSID just in case
#  - ssid: !secret wifi_ssid
#    password: !secret wifi_password
#    priority: 1
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "$devicename Fallback Hotspot"
    password: !secret hotspot_password
    ap_timeout: 10min

captive_portal:

time:
  - platform: homeassistant
#    timezone: "Australia/Melbourne" - change this to suit or remove it entirely and let it work it out itself
    id: esptime

external_components:
  - source:
      type: git
      url: https://github.com/velaar/esphome
      ref: dev
    components: [ waveshare_epaper, display]

dallas:
  - pin: $dapin

# These are used to create a number entity that can then
# be changed via the esphome device that HA can use to
# perform an action
number:
  # This is to adjust the virtual thermostat
  - platform: template
    optimistic: true
    name: "$devicename thermostat"
    id: thermostat_adjust
    min_value: 18
    max_value: 35
    initial_value: 20
    update_interval: 1s
    step: 1
  # This is a binary off or on to toggle the virtual thermostat states between off or heat or cool
  # Where 0 is off and 1 is heat and 2 is cool
  - platform: template
    optimistic: true
    name: "$devicename therm state"
    id: thermostat_state
    min_value: 0
    max_value: 2
    initial_value: 0
    update_interval: 1s
    step: 1
  # This is a binary off or on to toggle the duct off or on
  # Where 0 is off and 1 is on - note that if the thermostat is on then this is overruled 
  - platform: template
    optimistic: true
    name: "$devicename duct state"
    id: duct_state
    min_value: 0
    max_value: 1
    initial_value: 0
    update_interval: 1s
    step: 1

sensor:
  - platform: uptime
    id: uptime_sensor
    update_interval: 60s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(uptime_sensor).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 ? String(days) + ":" : "000:") +
                (hours ? String(hours) + ":" : "00:") +
                (minutes ? String(minutes) + ":" : "00:") +
                (String(seconds) + "")
              ).c_str();    
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    id: wifisignal
    update_interval: 60s
    unit_of_measurement: dBm
    accuracy_decimals: 0
    device_class: signal_strength
    state_class: measurement
    entity_category: diagnostic
  - platform: copy # Reports the WiFi signal strength in %
    source_id: wifisignal
    id: wifipercent
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
#  - platform: dallas
#    address: paste in here the address you see in the log after the first boot then remove the hash 
#    name: "$devicename Temp"
  - platform: uptime
    name: "$devicename Uptime"
# Change for room temp sensor 
  - platform: homeassistant
    id: current_temperature
    entity_id: sensor.wall_control_04_wall04_temp
#    attribute: median
  - platform: homeassistant
    id: outdoor_temperature
    entity_id: sensor.gw1000_v1_7_6_outdoor_temperature
  - platform: homeassistant
    id: max_temperature
    entity_id: sensor.brighton_east_temp_max_0
  - platform: homeassistant
    id: min_temperature
    entity_id: sensor.brighton_east_temp_min_1
# Change for room thermostat  
  - platform: homeassistant
    id: thermostat_temperature
    entity_id: climate.thermostat_master
    attribute: temperature
  - platform: homeassistant
    id: house_thermostat_temperature
    entity_id: climate.rinnai_touch
    attribute: temperature

binary_sensor:
  - platform: gpio
    id: wall_button_1
    name: "Wall Button 1"
    pin:
      number: $b1pin
      mode:
        input: true
        pullup: true # Taking advantage of the pullup resistor in the ESP32
      inverted: true # Inverted true means will be ON when button pressed
#    filters:
# Use one if required - I did not need to
#      - delayed_on: 10ms
#      - delayed_off: 10ms
    on_press:
      - display.page.show_next: mydisplay
      - component.update: mydisplay
  - platform: gpio
    id: wall_button_2
    name: "Wall Button 2"
    pin:
      number: $b2pin
      mode:
        input: true
        pullup: true
      inverted: true
    on_press:
    - if:
        condition:
          display.is_displaying_page: page1
        then:
          - logger.log: "Button2 pressed on page1"
          - number.increment: thermostat_state
    - if:
        condition:
          display.is_displaying_page:
            id: mydisplay
            page_id: page2
        then:
          - logger.log: "Button2 pressed on page2"
          - number.decrement: thermostat_adjust
    - if:
        condition:
          display.is_displaying_page:
            id: mydisplay
            page_id: page3
        then:
          - logger.log: "Button2 pressed on page3"

  - platform: gpio
    id: wall_button_3
    pin: 
      number: $b3pin
      mode:
        input: true
        pullup: true
      inverted: true
    name: "Wall Button 3"
    on_press:
    - if:
        condition:
          display.is_displaying_page: page1
        then:
          - logger.log: "Button3 pressed on page1"
          - number.increment: duct_state
# Note that default cycle is true meaning that the number will cycle between 0 and 1
    - if:
        condition:
          display.is_displaying_page:
            id: mydisplay
            page_id: page2
        then:
          - logger.log: "Button3 pressed on page2"
          - number.increment: thermostat_adjust
    - if:
        condition:
          display.is_displaying_page:
            id: mydisplay
            page_id: page3
        then:
          - logger.log: "Button3 pressed on page3"


text_sensor:
  # ESP Home UpTime
  - platform: template
    id: uptime_human
    icon: mdi:clock-start
  - platform: wifi_info
    ip_address:
      name: "$devicename IP Address"
      id: ipaddress
    ssid:
      name: "$devicename Connected SSID"
      id: ssid
    bssid:
      name: "$devicename Connected BSSID"
      id: bssid
    mac_address:
      name: ESP Mac Wifi Address
      id: mac
    scan_results:
      name: ESP Latest Scan Results
      id: scan
  - platform: homeassistant
    id: duct
    entity_id: switch.hvac_vav06
# Change for room thermostat  
  - platform: homeassistant
    id: hvac
    entity_id: climate.thermostat_master
  - platform: homeassistant
    id: house_hvac
    entity_id: climate.rinnai_touch

font:
  - file: "gfonts://Roboto"
    id: font1
    size: 20
  - file:
      type: gfonts
      family: Roboto
      weight: 900
    id: font2
    size: 20
    glyphs: 
      ['&', '@', '!', ',', '.', '"', '%', '+', '-', '_', ':', '°', '0', '?',
       '1', '2', '3', '4', '5', '6', '7', '8', '9', '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', '/', 'ä', 'ö', 'ü', 'Ä', 'Ü', 'Ö', '']
  - file:
      type: gfonts
      family: Roboto
      weight: 900
    id: font3
    size: 70
  - file:
      type: gfonts
      family: Roboto
      weight: 900
    id: font4
    size: 50
  - file: 'fonts/materialdesignicons-webfont.ttf'
    id: weather_font_15
    size: 15

spi:
  clk_pin: $clpin
  mosi_pin: $mopin
#  miso_pin: D7 #not connected

display:
  - platform: waveshare_epaper
    cs_pin: $cspin
    dc_pin: $dcpin
    busy_pin: $bupin
    reset_pin: $repin
    model: 2.90inV2
    id: mydisplay
    update_interval: 5s
    full_update_every: 60
    rotation: 90°

    pages:
# Following shows screen updates during booting
# Disabled as screens are persistent ie need to cycle through them afterwards
#      - id: boot0
#        lambda: |-
#          it.print(0, 10, id(font2), "Booting");
#      - id: boot1
#        lambda: |-
#          it.print(0, 10, id(font2), "WiFi ON");
#      - id: boot2
#        lambda: |-
#          it.print(0, 10, id(font2), "HA ON");

# Notes
# Display is 296 pixels wide by 128 high
      - id: page1
        lambda: |-
          //Header
          it.strftime(0, 0, id(font2), TextAlign::TOP_LEFT, "%a %H:%M", id(esptime).now());
          it.printf(115, 0, id(font2), TextAlign::TOP_LEFT, "THERM");          
          it.printf(210, 0, id(font2), TextAlign::TOP_LEFT, "DUCT");
          it.printf(0, 25, id(font3), "%.1f°", id(current_temperature).state);          
          it.printf(200, 25, id(font3), "%s", id(duct).state.c_str());
          it.printf(0, 100, id(font2), "Thermostat: %s", id(hvac).state.c_str());
          // Draw a rectangle around duct info with the top left at 193 to right, 30 down, width of 102 and a height of 95
          it.rectangle(193, 30, 102, 95);
          // Draw a rectangle around outside temp info with the top left at 98 to right, 0 down, width of 95 and a height of 25
          it.rectangle(103, 0, 95, 25);

      - id: page2
        lambda: |-
          it.printf(0, 0, id(font2), TextAlign::TOP_LEFT, "Next Page");
          it.printf(120, 0, id(font2), TextAlign::TOP_LEFT, "DOWN");
          it.printf(240, 0, id(font2), TextAlign::TOP_LEFT, "UP");
          it.printf(0, 25, id(font4), "%.1f°", id(thermostat_temperature).state);
          it.printf(150, 25, id(font4), "%s", id(hvac).state.c_str());
          it.printf(0, 100, id(font2), TextAlign::TOP_LEFT, "Room Thermostat");

      - id: page3
        lambda: |-
          it.printf(0, 0, id(font2), TextAlign::TOP_LEFT, "Next Page");
          it.printf(0, 25, id(font4), "%.1f°", id(house_thermostat_temperature).state);
          it.printf(150, 25, id(font4), "%s", id(house_hvac).state.c_str());
          it.printf(0, 100, id(font2), "House Thermostat");

      - id: page4
        lambda: |-
          it.printf(0, 0, id(font2), TextAlign::TOP_LEFT, "Next Page");
          it.printf(0, 25, id(font4), "Now: %.1f°", id(outdoor_temperature).state);
          it.printf(0, 75, id(font2), "Min/Max: %.1f/%.1f°", id(min_temperature).state,id(max_temperature).state);
          it.printf(0, 100, id(font2), "Weather");

      - id: page5
        lambda: |-
          it.printf(50, 100, id(font2), TextAlign::TOP_LEFT, "Inside, now %.1f°", id(current_temperature).state);     
          it.graph(0, 0, id(temp_graph_h));

      - id: page6
        lambda: |-
          it.printf(0, 0, id(font2), TextAlign::TOP_LEFT, "Home");
          it.printf(5, 20, id(font2), "ssid: %s", id(ssid).state.c_str());
          it.printf(5, 40, id(font2), "WiFi: %.f dBm %.f percent", id(wifisignal).state, id(wifipercent).state);
          it.printf(5, 60, id(font2), "Uptime: %s", id(uptime_human).state.c_str());
          it.printf(0, 100, id(font2), "Info");


graph:
  - id: temp_graph_h
    duration: 24h
    min_value: 10
    max_value: 35
    width: 294
    height: 120
    border: True
    traces:
      - sensor: current_temperature
        line_type: SOLID
  - id: temp_graph_d
    duration: 24h
    width: 294
    height: 120
    border: True
    traces:
      - sensor: current_temperature
        line_type: SOLID

Then you need to create some thermostats for each room by adding them into your configuration.yaml file (in my case I’m using “dualmode_generic” ) eg:

climate:
  - platform: dualmode_generic
    name: Thermostat-Master
    unique_id: climate.thermostat_master
    heater: switch.hvac_vav06
    cooler: switch.hvac_vav06
    target_sensor: sensor.wall_control_04_wall04_temp
    reverse_cycle: cooler, heater
    min_temp: 18
    max_temp: 28
    cold_tolerance: 0.4
    hot_tolerance: 0.4
    min_cycle_duration:
        minutes: 5

Then you also need to add in some automations that do actions based on changes in state of the esphome sensors such as:

alias: HVAC - Master - toggle thermostat
description: ""
trigger:
  - platform: state
    entity_id:
      - number.wall_control_04_wall04_therm_state
condition: []
action:
  - if:
      - condition: state
        entity_id: number.wall_control_04_wall04_therm_state
        state: "0.0"
    then:
      - service: climate.set_hvac_mode
        data:
          hvac_mode: "off"
        target:
          entity_id: climate.thermostat_master
  - if:
      - condition: state
        entity_id: number.wall_control_04_wall04_therm_state
        state: "1.0"
    then:
      - service: climate.set_hvac_mode
        data:
          hvac_mode: heat
        target:
          entity_id: climate.thermostat_master
  - if:
      - condition: state
        entity_id: number.wall_control_04_wall04_therm_state
        state: "2.0"
    then:
      - service: climate.set_hvac_mode
        data:
          hvac_mode: cool
        target:
          entity_id: climate.thermostat_master
mode: single

1 Like

…and dang. There was an update to esphome (Move ESPTime into core esphome namespace by jesserockz · Pull Request #4926 · esphome/esphome · GitHub) that broke the code in the custom component. Will compile by removing the custom component section, but then the screen is corrupted. Need to make some manual hacks to get it working again (Waveshare ePaper 2.9" Rev2.1 - flickering screen refreshing problem. · Issue #2334 · esphome/issues · GitHub). Can sorta temporary ‘fix’ it by getting it to refresh every second, but that’s a very very temporary fix.

…and the solution was to:

  1. Create a directory in /config/esphome/my_components
  2. Within that directory create two more: /config/esphome/my_components/display and /config/esphome/my_components/waveshare_epaper
  3. Download the contents of esphome/esphome/components/display at dev · velaar/esphome · GitHub and put that into /config/esphome/my_components/display
  4. Download the contents of esphome/esphome/components/waveshare_epaper at dev · velaar/esphome · GitHub and put that into /config/esphome/my_components/waveshare_epaper
  5. Edit the two files /config/esphome/my_components/display/display_buffer.cpp and /config/esphome/my_components/display/display_buffer.h and change the four references in each file from “time::ESPTime time” to “ESPTime time” (Thanks Kaibob2 - https://github.com/esphome/issues/issues/2334#issuecomment-1613234363)
  6. Change the external component reference in your code to point to the local repository rather than the one on github eg:
external_components:
  - source:
      type: local
      path: /config/esphome/my_components
  1. Do a “clean build files”
  2. Validate & install

Hopefully at some point in time the changes in the files to support the new waveshare epaper display will finally make it into the release version, in which case we can simply remove the external_components section and those local files.

So my setup has now been running for over five months and I am finding it to be nice and reliable. There was one small issue caused by me when I added some more controllers - I accidentally disconnected one of the 24v wires going to one of the relays. The result being that as far as esphome was concerned everything was fine but the VAVs on that relay were not opening/closing as they should. Simple fix once I discovered there was an issue.

Some other notes:

  • The esp32 board has a power light that continually glows when the board is connected to power. This meant that at night there was a faint blue light coming from the controller. This cannot be disabled by code so the quickest ‘fix’ was to use some needle nose pliers and to (carefully) crush the led itself. Kinda brutal, but no real choice.
  • I also found that with the esp32 boards I got, unlike my standard go-to Wemos D1 mini, they did not reliably connect to my wifi (which at the time had both 2.4 and 5ghz signals running with the same SSID). I created a new SSID for just 2.4ghz IoT devices and now the connection is rock solid.
  • I was concerned that the temp sensor would pickup heat from the esp32 board but mounting it away from the board and low down with the sensor next to a grill seemed to be good enough. I compared the room temps to some separate sensors and they matched within a few fractions of a degree.
  • The case I made is, I will readily admit, fairly basic. I ended up hot-gluing some magnets so the two halves clip together, but if I keep these for any significant length of time I will likely redesign the case to be a lot more sleek - perhaps with slim line or touch buttons. I could also easily reduce the depth of the case by directly soldering everything and/or by even hiding a lot of the internals inside the wall itself. That’s all for future me to worry about - for now they work which is the main thing! I do have four more rooms to add controllers to, but they aren’t critical so I can take my time. I might play with one of the round displays (similar to a Nest controller) or one of the square displays (eg zx3d95ce01s-tr) designed to mount into a standard 86mm square wall box. Combined with a motion sensor to turn the screen on/off they should look fairly professional.

So FWIW I just ordered an “ESP32 LVGL for Arduino Development Board 1.28 Inch IPS Smart Display 240*240 LCD TFT Module WiFi & Bluetooth Optional Touch” - the touch version with a case. There is arduino code available for it but I suspect it will be a challenge to get it to work under esphome but hey. It’s a hobby. Capacitive touch, 1.28 inch equates to 3.25 cm (roughly) but the screen module with cable is 38mm so including case it should be around the 4cm mark - so like a medium sized smart watch. My plan would be to simply use it to display the current temp (from a separate sensor, although it does have a 4 pin jst sh connector with 3.3v, gnd, tx, and rx so could probably use that), as well as change the set temp - should be able to squeeze that onto the screen.

My test controller arrived - I’ll document it here

Watching with interest - thinking of doing the same, ie, controlling some dampers to maintain temps in each zone.

Minor update. The display is now supported natively so does not need to use an external component. I did have to play around to find the version that actually supported my screen, as what I thought were the obvious choices caused the screen to go into a blurred mode that didn’t refresh properly. The magic was:

display:
  - platform: waveshare_epaper
    cs_pin: $cspin
    dc_pin: $dcpin
    busy_pin: $bupin
    reset_pin: $repin
    model: 2.90inv2-r2
    update_interval: 30s
    reset_duration: 2ms
    id: mydisplay
    rotation: 90°

Having said all that, the cheaper colour watch device has actually got good feedback from my test subjects (basically everyone wants me to replace the e-paper screens in their rooms with the smaller round screen) so I guess I’m going to end up with some spare e-paper panels I can use for other projects!

…and ordered & installed some more controllers - so now I have seven of the smaller (and cheaper) watch screen controllers on the walls. One room stopped working though - assumed it was my cabling or some other dumb thing I’d done - after much cable tracing and testing, finally discovered one of the dampers being dead. Suspect that the motor has fused. Annoying, but fortunately it was not too hard to reach. I’ve also realised that I have one old controller left in a bathroom, so will need to order another screen. Dang.

…and yep, confirmed. For once it wasn’t something stupid that I’d done, but just that the motor in one of the dampers had died. Bought a new one, replaced (which sounds a lot easier than it was!), and all is good again. I now have the touch screens working for all the rooms except for a bathroom where I’m using my first e-ink display, partly because I ran out of the colour displays and partly because if I used the smaller colour display there it would leave two holes in the tiles visible from where the original controller was. The other locations are all on plasterboard, so is easy to patch so the holes will become invisible next time we paint the walls. Here are the final versions:


Something for future me to worry about is tidying up the nest of cables, although it’s a lot better now that I’ve removed the old system and mounted my kit on one of the joists rather than simply dumped it in a pile on the plaster ceiling that the original installer had done.