View shared configuration
# =============================================================================
# reTerminal Sticky - Home Assistant wall controller
# Automation Addict | v22
#
# +------------+------------+
# | TILE 1 | TILE 2 |
# +------------+------------+
# | TILE 3 | TILE 4 |
# +------------+------------+
# | Voice | Note |
# +-------------------------+
#
# EVERYTHING YOU NEED TO CHANGE IS IN THE BLOCK DIRECTLY BELOW.
# Nothing under `# ---- machinery ----` needs touching to re-skin this for
# your own house.
#
# Tiles 1-3 toggle anything toggleable - light, switch, fan, input_boolean,
# media_player, script. Tile 4 is the cover slot (blinds, curtains, garage).
# Want a fourth toggle instead of a cover? See SWAP-GUIDE.md.
#
# Icons are Material Design Icon NAMES. Browse them at
# https://pictogrammers.com/library/mdi/ and paste the name in - no
# codepoints, no downloads, ESPHome fetches the SVG when it builds.
# =============================================================================
#
# ---------------------------------------------------------------------------
# BEFORE YOUR FIRST BUILD - secrets
# ---------------------------------------------------------------------------
# This file ships with NO credentials in it. Everything sensitive lives in
# `secrets.yaml`, which sits next to this file in your ESPHome config folder
# and never gets shared.
#
# 1. Generate an API encryption key. Easiest route is the ESPHome dashboard
# (it offers to make one when you add a device), or run:
# python3 -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"
# Paste the result into the `api_encryption_key` line below.
#
# 2. Create `secrets.yaml` with these four lines:
#
# wifi_ssid: "YourNetworkName"
# wifi_password: "YourWiFiPassword"
# ota_password: "something-long-and-random"
# api_encryption_key: "paste-the-44-character-key-here"
#
# 3. Add `secrets.yaml` to your `.gitignore` before you commit anything.
#
# Note: OTA has its own password here rather than reusing the Wi-Fi one.
# They are separate credentials and should stay that way.
# ---------------------------------------------------------------------------
substitutions:
# ---------------------------------------------------------------------------
# >>>>>>>>>>>>>>>>>>>>>> EDIT FROM HERE <<<<<<<<<<<<<<<<<<<<<<
# ---------------------------------------------------------------------------
device_name: sticky_eink_v1
friendly_name: "Sticky_Eink_v1"
footer_text: "captains quarters"
# ---- TILE 1 (top left) ----------------------------------------------------
tile1_entity: input_boolean.music_transfer
tile1_label: "Music"
tile1_icon: music
# ---- TILE 2 (top right) ---------------------------------------------------
tile2_entity: input_boolean.lighting_control
tile2_label: "Lighting"
tile2_icon: ceiling-light
# ---- TILE 3 (bottom left) -------------------------------------------------
tile3_entity: input_boolean.cooking_mode_bypass
tile3_label: "Cooking"
tile3_icon: chef-hat
# ---- TILE 4 (bottom right) - THE COVER SLOT -------------------------------
# Must be a `cover.` entity. Tapping it opens or closes, whichever is the
# opposite of where it is now.
tile4_entity: cover.living_room_blinds
tile4_label: "Heating"
tile4_icon: radiator
# ---- BOTTOM ROW: TALK and NOTE --------------------------------------------
voice_label: "TALK"
voice_icon: home-assistant
# The note is stored in Home Assistant, not on the device. Create a helper:
# Settings > Devices & Services > Helpers > Create helper > Text
# Name it "Sticky Note", set Maximum length to 255.
# Storing it in HA rather than on the device means it survives deep sleep
# AND you can set or clear the note from your phone while you are out.
note_entity: input_text.sticky_note
note_label: "NOTE"
note_icon: note-edit-outline
# Cosmetics for the note card.
# The little icon above the greeting, top-left of the card.
note_accent_icon: pin
note_greeting: "Message waiting"
# The big doodle in the bottom-right corner of the card.
note_doodle_icon: home-assistant
# ---- LOW POWER MODE -------------------------------------------------------
# "true" - sleeps after the idle timeout below, wakes on any button press.
# Days of battery, but while asleep it cannot hear Home
# Assistant, so the tiles show the last known states until
# something wakes it.
# "false" - never sleeps. Instantly responsive and always live, but you
# will be charging it roughly daily.
#
# Compile-time on purpose. This used to be a switch in Home Assistant, and
# a stored "off" silently overrode the config - which is a miserable thing
# to debug, because the file says one thing and the device does another.
# Now the file is the only answer. Changing it means a reflash; knowing
# what your device will do means reading one line.
low_power: "true"
awake_after_button_ms: "300000" # 5 min awake after you touch it
awake_after_timer_ms: "25000" # 25 s awake on a self-triggered refresh
sleep_duration: 360min # how often it wakes itself to catch up
tile1_state: input_text.sticky_room
tile2_state: input_text.sticky_mode
# ---------------------------------------------------------------------------
# >>>>>>>>>>>>>>>>>>>>>> EDIT TO HERE <<<<<<<<<<<<<<<<<<<<<<
# Everything past this point is machinery.
# ---------------------------------------------------------------------------
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
on_boot:
# -------------------------------------------------------------------------
# PRIORITY 1000 - runs before ANY component sets up, display included
# (the display initialises at HARDWARE priority, 800).
#
# Why this exists: ESPHome puts the SSD1677 controller into its OWN deep
# sleep mode after each refresh, and the datasheet is clear that the only
# way out of that state is a HARDWARE reset. On an MCU deep-sleep wake the
# panel was never power-cycled - it is still sitting in controller deep
# sleep - so ESPHome's init sequence talks to a chip that is not listening.
# The result is a frame with inverted polarity and horizontal streaks,
# because the waveform LUT never loaded.
#
# So we cold-start the panel by hand here: drop its enable rail, hold
# reset low, let the rails collapse, then bring it back. By the time the
# display component initialises it is talking to a genuinely fresh
# controller.
#
# Pin numbers come from ESPHome's own board preset for this model:
# enable 47 reset 17 cs 15 dc 16 busy 18
# -------------------------------------------------------------------------
- priority: 1000
then:
- lambda: |-
// 1. Release anything the deep-sleep pad hold was pinning.
gpio_deep_sleep_hold_dis();
gpio_hold_dis((gpio_num_t) 45);
gpio_hold_dis((gpio_num_t) 46);
// 2. Hard power-cycle the ePaper panel.
const gpio_num_t EPD_EN = (gpio_num_t) 47;
const gpio_num_t EPD_RST = (gpio_num_t) 17;
gpio_hold_dis(EPD_EN);
gpio_hold_dis(EPD_RST);
gpio_reset_pin(EPD_EN);
gpio_reset_pin(EPD_RST);
gpio_set_direction(EPD_EN, GPIO_MODE_OUTPUT);
gpio_set_direction(EPD_RST, GPIO_MODE_OUTPUT);
gpio_set_level(EPD_RST, 0); // hold controller in reset
gpio_set_level(EPD_EN, 0); // and kill its supply
delay(150); // let the rails actually collapse
gpio_set_level(EPD_EN, 1); // power back up
delay(30);
gpio_set_level(EPD_RST, 1); // release reset
delay(30);
ESP_LOGI("epd", "panel cold-started");
- priority: 600
then:
- lambda: |-
// A button wake means a person is here - stay up for the full
// idle window. A timed wake is just a refresh, so get in and out.
auto cause = esp_sleep_get_wakeup_cause();
bool by_button = (cause == ESP_SLEEP_WAKEUP_EXT1);
id(awake_ms) = by_button ? ${awake_after_button_ms} : ${awake_after_timer_ms};
id(note_sleep_card) = false;
ESP_LOGI("sleep", "wake cause=%d awake_for=%d ms", (int) cause, id(awake_ms));
- deep_sleep.prevent: sleeper
- output.turn_on: pwr_hold
- output.turn_on: pwr_lock
- switch.turn_on: touch_power_enable
- output.turn_on: mic_power_enable
- output.turn_on: charge_enable
- pcf8563.read_time:
- priority: -100
then:
- delay: 4s
# Two full refreshes on wake. ePaper needs one pass to flush whatever
# the controller was left holding and a second to lay down a clean
# frame - cheap insurance against a half-initialised panel, and it
# costs a couple of seconds only on wake, not during normal use.
# Two refreshes, spaced well past a full cycle. The old version had
# them 1.5 s apart, so the second and third were simply rejected -
# "Display already in state POWER_OFF" in your log was exactly that.
- component.update: eink
- delay: 10s
- component.update: eink
- script.execute: idle_timer
esp32:
board: esp32-s3-devkitc-1
variant: ESP32S3
flash_size: 32MB
framework:
type: arduino
advanced:
enable_idf_experimental_features: true
logger:
hardware_uart: UART0
level: INFO
api:
encryption:
# Generate your own - see the "BEFORE YOUR FIRST BUILD" note at the top.
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret wifi_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
power_save_mode: LIGHT
ap:
ssid: "${device_name} Setup"
captive_portal:
spi:
clk_pin: GPIO13
mosi_pin: GPIO14
miso_pin: GPIO12
i2c:
- id: i2c_sensor
scl: GPIO0
sda: GPIO1
frequency: 400kHz
# Boot log prints "Found i2c device at address 0x.." for each chip.
# Expect 0x44 (SHT40), 0x51 (RTC), 0x55 (BQ27220), 0x6A (IMU).
# If 0x55 is absent, the fuel gauge is not answering and no amount of
# register fiddling will help.
scan: true
- id: i2c_touch
scl: GPIO2
sda: GPIO3
frequency: 100kHz
i2s_audio:
i2s_lrclk_pin: GPIO19
# =============================================================================
# DEEP SLEEP
#
# Wakes on ANY of the three physical buttons (OK/UP/DOWN), and once an hour on
# its own to refresh the clock and entity states. Sleeps again after the idle
# timer expires.
#
# READ THIS BEFORE FLASHING - the one way this bites:
# GPIO45 and GPIO46 are the power latch. In deep sleep, ordinary GPIOs stop
# driving, the latch opens, and the device powers all the way off instead of
# sleeping. `gpio_deep_sleep_hold_en()` in the go_to_sleep script pins them
# where they are. If that ever fails you have not bricked anything - OK is the
# physical power button, so a long press brings it back.
# =============================================================================
deep_sleep:
id: sleeper
esp32_ext1_wakeup:
pins:
- number: GPIO4
mode: INPUT_PULLUP
allow_other_uses: true
- number: GPIO5
mode: INPUT_PULLUP
allow_other_uses: true
- number: GPIO6
mode: INPUT_PULLUP
allow_other_uses: true
mode: ANY_LOW
sleep_duration: ${sleep_duration}
output:
- platform: gpio
pin: GPIO45
id: pwr_hold
- platform: gpio
pin: GPIO46
id: pwr_lock
- platform: gpio
pin: GPIO38
id: mic_power_enable
# BQ25616 charger enable, active LOW. This was missing - without driving it
# the charge path is left to whatever the pin defaults to. Turning this
# output ON drives the pin LOW, which enables charging.
- platform: gpio
pin:
number: GPIO39
inverted: true
id: charge_enable
- platform: ledc
pin: GPIO48
id: buzzer_pwm
frequency: 2700Hz
switch:
# ---------------------------------------------------------------------------
# Touch axis correction, adjustable at runtime from Home Assistant.
# These stay switches on purpose: calibration is something you fiddle with
# once, and toggling beats a reflash-and-guess cycle. Unlike a power mode,
# getting one wrong is instantly obvious - the wrong tile fires.
# ---------------------------------------------------------------------------
- platform: template
id: flip_x
name: "Touch Flip X"
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
entity_category: config
- platform: template
id: flip_y
name: "Touch Flip Y"
optimistic: true
# SETTLED: this board needs Y flipped and X left alone.
restore_mode: RESTORE_DEFAULT_ON
entity_category: config
- platform: gpio
pin: GPIO42
id: touch_power_enable
internal: true
restore_mode: ALWAYS_ON
setup_priority: 1100
light:
- platform: monochromatic
output: buzzer_pwm
id: buzzer
internal: true
default_transition_length: 0s
time:
- platform: pcf8563
i2c_id: i2c_sensor
id: rtc_time
address: 0x51
update_interval: never
- platform: homeassistant
id: ha_time
on_time_sync:
then:
- pcf8563.write_time:
# ui_mode: 0 menu, 1 listening, 2 thinking, 3 reply
globals:
- id: ui_mode
type: int
restore_value: false
initial_value: '0'
- id: awake_ms
type: int
restore_value: false
initial_value: '${awake_after_button_ms}'
- id: hit_tile
type: int
restore_value: false
initial_value: '-1'
# True while a voice session is capturing a NOTE rather than a command.
- id: note_capture
type: bool
restore_value: false
initial_value: 'false'
# True when the note card is being drawn as the sleep screen, so we leave
# the buttons off it - nothing can be pressed while it is asleep.
- id: sleep_redraw
type: bool
restore_value: false
initial_value: 'false'
- id: note_sleep_card
type: bool
restore_value: false
initial_value: 'false'
- id: last_heard
type: std::string
restore_value: false
initial_value: '""'
- id: last_reply
type: std::string
restore_value: false
initial_value: '""'
font:
- file: "gfonts://Inter@700"
id: f_tile
size: 27
- file: "gfonts://Inter@700"
id: f_state
size: 21
- file: "gfonts://Inter@700"
id: f_head
size: 36
- file: "gfonts://Inter@400"
id: f_body
size: 24
# Three sizes for the note. Short notes get the big one; the lambda steps
# down only if the text will not fit.
- file: "gfonts://Inter@600"
id: f_note_xl
size: 50
- file: "gfonts://Inter@600"
id: f_note_lg
size: 40
- file: "gfonts://Inter@500"
id: f_reply
size: 31
- file: "gfonts://Inter@400"
id: f_small
size: 17
# BINARY + chroma_key transparency = only the glyph pixels are drawn, no
# background box. (TRANSPARENT_BINARY was the old spelling; ESPHome removed it.)
image:
- id: ic_tile1
file: mdi:${tile1_icon}
resize: 96x96
type: BINARY
transparency: chroma_key
- id: ic_tile2
file: mdi:${tile2_icon}
resize: 96x96
type: BINARY
transparency: chroma_key
- id: ic_tile3
file: mdi:${tile3_icon}
resize: 96x96
type: BINARY
transparency: chroma_key
- id: ic_tile4
file: mdi:${tile4_icon}
resize: 96x96
type: BINARY
transparency: chroma_key
- id: ic_mic
file: mdi:${voice_icon}
resize: 64x64
type: BINARY
transparency: chroma_key
- id: ic_listening
file: mdi:microphone-message
resize: 160x160
type: BINARY
transparency: chroma_key
- id: ic_thinking
file: mdi:dots-horizontal-circle-outline
resize: 160x160
type: BINARY
transparency: chroma_key
- id: ic_back
file: mdi:arrow-left
resize: 40x40
type: BINARY
transparency: chroma_key
- id: ic_note
file: mdi:${note_icon}
resize: 64x64
type: BINARY
transparency: chroma_key
- id: ic_accent
file: mdi:${note_accent_icon}
resize: 34x34
type: BINARY
transparency: chroma_key
- id: ic_doodle
file: mdi:${note_doodle_icon}
resize: 120x120
type: BINARY
transparency: chroma_key
- id: ic_trash
file: mdi:trash-can-outline
resize: 40x40
type: BINARY
transparency: chroma_key
- id: ic_reply
file: mdi:message-text-outline
resize: 88x88
type: BINARY
transparency: chroma_key
sensor:
- platform: sht4x
i2c_id: i2c_sensor
address: 0x44
temperature:
name: "Temperature"
id: temp_sensor
humidity:
name: "Humidity"
id: hum_sensor
update_interval: 120s
- platform: template
id: battery_level
name: "Battery Level"
unit_of_measurement: "%"
device_class: battery
state_class: measurement
accuracy_decimals: 0
update_interval: 120s
lambda: |-
// BQ27220 StateOfCharge, standard command 0x2C. No unseal needed.
uint8_t reg = 0x2C;
uint8_t raw[2] = {0, 0};
auto err = id(i2c_sensor).write_readv(0x55, ®, 1, raw, 2);
if (err != esphome::i2c::ERROR_OK) {
ESP_LOGW("battery", "BQ27220 SoC read failed, i2c error=%d", (int) err);
return NAN;
}
return static_cast<float>(static_cast<uint16_t>(raw[0]) |
(static_cast<uint16_t>(raw[1]) << 8));
# Voltage and current are the diagnostic pair. If these read fine but SoC
# does not, it is a register problem. If all three fail, the gauge is not on
# the bus at all - check the scan lines in the boot log.
- platform: template
id: battery_voltage
name: "Battery Voltage"
unit_of_measurement: "mV"
device_class: voltage
state_class: measurement
accuracy_decimals: 0
update_interval: 120s
lambda: |-
uint8_t reg = 0x08; // Voltage()
uint8_t raw[2] = {0, 0};
auto err = id(i2c_sensor).write_readv(0x55, ®, 1, raw, 2);
if (err != esphome::i2c::ERROR_OK) return NAN;
return static_cast<float>(static_cast<uint16_t>(raw[0]) |
(static_cast<uint16_t>(raw[1]) << 8));
- platform: template
id: battery_current
name: "Battery Current"
unit_of_measurement: "mA"
device_class: current
state_class: measurement
accuracy_decimals: 0
update_interval: 120s
lambda: |-
uint8_t reg = 0x0C; // AverageCurrent(), signed
uint8_t raw[2] = {0, 0};
auto err = id(i2c_sensor).write_readv(0x55, ®, 1, raw, 2);
if (err != esphome::i2c::ERROR_OK) return NAN;
uint16_t v = static_cast<uint16_t>(raw[0]) | (static_cast<uint16_t>(raw[1]) << 8);
return static_cast<float>(static_cast<int16_t>(v)); // positive = charging
- platform: wifi_signal
name: "Wi-Fi Signal"
update_interval: 120s
entity_category: diagnostic
# -----------------------------------------------------------------------------
# PDM microphone
#
# ESPHome's defaults here are wrong for a single PDM mic and produce exactly
# what you saw: 15 seconds of listening, pure silence, and Whisper filling the
# void with "Thank you."
#
# channel default is `right`. A single PDM mic with its SELECT pin
# tied low sits on the LEFT channel - which is the usual
# wiring. Reading the right channel gives you digital
# silence, not quiet audio. THIS IS THE PRIME SUSPECT.
# bits_per_sample default is `32bit`. PDM mics want 16.
# correct_dc_offset PDM output
Discussion 0
Add a commentStart the conversation
Share an example, ask a question or explain how you would use this.