…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