Once the T-Display gets connected to an external LiIon or LiPo battery, it can run independently and requires no USB cable. At an average power consumption of 130mA, it can theoretically run short of 8 hours from a 1.000 mAh battery.
Battery-driven boards introduce new challenges:
- Power Switch: you want to be able to turn the device off when you don’t need it
- Power Management: you may want to switch the board to low-power modes when it isn’t busy doing things for you
- Low Voltage Protection: you want to turn off the device when battery voltage drops below a voltage threshold that won’t allow the peripherals (like the display) to work correctly anymore
One solution not discussed here is to add a physical power switch to the positive lead of your battery - very advisable. Physically turning off the device is the most efficient power savings mode and allows you to store the device for months without losing significant charge capacity.
Overview
This article focuses on how to add deep sleep support to the T-Display board, and how to do it right. If you do it careless, deep sleep may consume a lot more power than anticipated, and drain your batteries much sooner than you expected. The configuration presented here is based on the sample configuration created earlier.
The configuration also exposes all crucial device information to Home Assistant, including a battery icon with the current battery charging state, and a button to remotely send the device to deep sleep:
I’ll cover the required optimizations first, then present the entire configuration.
General Optimizations
In the header part of your configuration, make sure you enable 16MB flash memory if your board has 16MB. Else, ESPHome will only use 4MB flash memory which is often not sufficient when working with displays.
esp32:
board: esp32dev
flash_size: 16MB # important: if your board has 16MB, unlock it!
framework:
type: arduino
If your device is being used in a stable WiFi, and not switching back and forth between different SSIDs, do this:
- Save RAM: Remove
captive_portal:
as well as theap:
access point. Removing the internal web server saves a significant amount of RAM. - Save Battery Power: Add
fast_connect: true
towifi:
. This skips scanning available WiFi networks (which doesn’t make sense when you always connect to the same SSID anyway). This speeds up WiFi connect, and saves valuable battery power, especially if you later use deep sleep and wake up your board only occasionally, i.e. to take sensor readings.
You may want to remove logger:
along with all log messages once you have debugged your device. This again saves battery power and minimizes WiFi transmissions.
This is what your header part should look like:
esphome:
name: t-display-1
friendly_name: T-Display #1
esp32:
board: esp32dev
flash_size: 16MB # important: if your board has 16MB, unlock it!
framework:
type: arduino
# Enable logging (optional)
logger:
# Enable Home Assistant API
api:
encryption:
key: "..."
ota:
- platform: esphome
password: "..."
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
Efficient Deep Sleep
When you send an ESP32 to deep sleep via ESPHome configuration, you often see code like this:
deep_sleep:
id: deep_sleep_control
wakeup_pin: 35
If you do it like this, the board would indeed switch to deep sleep once you invoke - deep_sleep.enter: deep_sleep_control
(i.e. via button press). It would now consume an astonishing 9mA though instead of the 370uA that it should take on this board, and here is why:
- Display Driver: once you add a display component to your configuration, the display controller gets initialized. Even when you send the ESP32 to deep sleep will this driver consume around 6.5mA.
- RTC IO Mode: when you use
wakeup_pin:
, you are automatically keeping the RTC IO subsystem active. This mode is calledext0
, and it costs you another 2mA. The better approach is to useesp32_ext1_wakeup:
: this mode can use multiple wakeup pins, however it only supports waking up the processor when all specified pins turn low together, or any one of them turns high. If that works for you, it saves another 2mA.
Correctly Adding Deep Sleep
Here is the most energy-efficient implementation for deep sleep and manual wake-up:
# enable deep sleep capabilities and set wakeup-pin in energy-efficient ext1 mode
deep_sleep:
id: deep_sleep_control
esp32_ext1_wakeup: # uses much less deep sleep power than wakeup_pin:
pins:
- number: 35
allow_other_uses: true
mode: ALL_LOW
Note how esp32_ext1_wakeup:
is used to enable the power efficient ext1
deep sleep mode: GPIO35 (one of the push buttons) is used to wake up the system. Since this push button is also used as a regular button elsewhere in the configuration, it is marked allow_other_uses: true
. Else, since ESPHome 2023 you would get errors, keeping you from assigning the same GPIO to two different components.
The pin mode is ALL_LOW
since the T-Display buttons are low active. Note that esp32_ext1_wakeup:
does not honor inverted: true
or any other sophisticated pin declarations (which is the primary difference to the more power-hungry wakeup_pin:
).
Invoking Deep Sleep
To invoke deep sleep, a script is used, because sending T-Display to deep sleep requires a few steps, not just one:
# perform all necessary actions to send peripherals to deep sleep
script:
- id: prepare_for_sleep
then:
# switch display controller in sleep mode:
- lambda: |-
// Send display to sleep before deep sleep
uint8_t command = 0x10; // Your command
id(display1).command(command);
- delay: 120ms
# send esp32 to deep sleep:
- deep_sleep.enter: deep_sleep_control
It is crucial to send the command SLEEPIN (0x10
) to the display to disable it before entering deep sleep. If you omit this, deep sleep will consume an extra 6.5mA.
I raised this issue with ESPHome, and hopefully we will see the display disabling itself automatically on deep sleep in the future.
The script can then be tied to any action you like: - script.execute: prepare_for_sleep
. You can for example use one button to send the board to deep sleep, and another one to wake it up again:
- Deep Sleep Button: invoke the script
prepare_for_sleep
when the button is pressed. - Wake Up Button: define the button pin in
esp32_ext1_wakeup:
.
Never use the same button for invoking deep sleep and wake up, or else the board will immediately wake up again as the button is still pressed when deep sleep takes over. Also, never use GPIO0 to send the board to deep sleep on a simple button press. GPIO0 is the boot button, and you don’t want the board to start deep sleeping when you really just wanted to boot into ROM bootloader mode. If you do use GPIO0 (like in this example), make sure deep sleep is invoked only on a long press.
When deep sleep is configured this way, it optimizes power efficiency:
The graph shows that the board consumes only 373uA in deep sleep. This is the best you can get with T-Display - even directly programmed c++ code on platformio does not perform better:
Invoking Deep Sleep
Deep sleep can now be invoked manually, automatically, and even remotely. The reason why the board entered deep sleep is published via a text_sensor:
:
# log system state: running or deep sleep (and reason)
- platform: template
name: "DeepSleep State"
id: sleep_state
icon: "mdi:power-sleep"
entity_category: "diagnostic"
device_class: ""
This is important because Home Assistant shows the last known parameters, and if the board was sent to deep sleep, none of its user controls respond anymore. That’s expected since a system in deep sleep is essentially turned off, but Home Assistant should report the fact that the system is currently sleeping - which is what this sensor does. It also reports why the system is sleeping.
Here are the three ways how the board can enter deep sleep:
Automatic Low Voltage Protection
LiIon and LiPo batteries shouldn’t be discharged below 3.2V to ensure longevity. Discharging lower than 3.2V wouldn’t make much sense anyway since the display starts to flicker below this voltage.
To send the device automatically to deep sleep when the battery voltage drops below 3.2V, add this:
- platform: adc
pin: GPIO34
id: battery_voltage
name: "Voltage"
update_interval: 1s
#internal: true
accuracy_decimals: 2
attenuation: 12dB
samples: 10
entity_category: "diagnostic"
device_class: "voltage"
filters:
# transform the raw value (voltage divider) to actual voltage:
- multiply: 2.04
# remove outliers (voltage spikes only):
- quantile:
window_size: 7
send_every: 4
send_first_at: 3
quantile: .25
# on new voltage readings, update battery statistics:
on_value:
then:
- component.update: battery_percent
- component.update: battery_status
# automatically enter deep-sleep below 3.2V:
on_value_range:
below: 3.2
then:
- text_sensor.template.publish:
id: sleep_state
state: "Deep Sleep (low voltage)"
- script.execute: prepare_for_sleep
Note how the sleep_state text_sensor:
is updated before the system enters deep sleep.
Reading Battery Voltage
Knowing the current battery voltage is crucial for battery-operated devices, and the previously discussed automatic deep sleep on low voltage is just one example. Displaying battery icons and status texts are other use cases.
To actually get a solid battery voltage reading requires some processing which is also taken care of by this sensor. Here is what the remaining code does:
- Automatic Deep Sleep:
on_value_range:
defines the threshold and executes the script to invoke deep sleep. - Automatic Update:
on_value:
makes sure dependent sensors are updated whenever the battery voltage changes, one of which shows the battery state of charge in percent, and the other one reports the power mode: USB or Battery. -
Filters: the battery sensor is really a simple ADC GPIO that measures the battery voltage across a voltage divider.
filters:
processes the raw readings to create a stable battery voltage. It first usesmultiply:
to account for the voltage divider. The factor is 2.0. For accurate readings, you should double-check your battery voltage, and make slight adjustments. In my case, the required factor was 2.04. There are always slight variations in resistor values. Since the sensor frequently produces voltage spikes, aquantile:
filter is used to cut off any value that is not in the bottom 25% of values, effectively removing sudden positive spikes. Make sure you switch the ADC toattenuation: 12dB
, or else your readings will be clipped, and you always measure a voltage around 1V.Here is a graph taken from Home Assistant, showing battery voltage over time. On the left, I used a median filter, and voltage spikes are still present. On the right is the 25% quantile filter with a nice and smooth linear discharge curve:
The flat line at the bottom represents the automatic low-voltage protection when the board switched to deep sleep once the voltage fell below 3.2V.
Make sure you switch the ADC to
attenuation: 12dB
, or else your readings will be clipped, and you always measure a voltage around 1V.
The battery voltage sensor is crucial for updating icons and status text, so update_interval:
is set to 1s. ESPHome sends updates over the network to Home Assistant only when the battery voltage really changes. Increasing the update interval is not recommended as it would increase the lag time when you connect or disconnect the device to USB power.
If you want to minimize network traffic, you can switch the sensor to internal: true
. You then miss out on the advanced voltage logging and analysis capabilities of Home Assistant, though.
Battery Status and Percentage
The raw battery voltage is picked up by two sensors:
battery_status is a text_sensor:
that translates a voltage reading into a status text:
text_sensor:
# log power mode: USB or battery
- platform: template
name: "Battery Status"
id: battery_status
entity_category: "diagnostic"
device_class: ""
icon: mdi:power-plug-battery
lambda: |-
if (id(battery_voltage).state > 4.74) {
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
id(display1).image(111, -3, id(powerPlug), YELLOW, BLACK);
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
id(display1).print(display_centerwidth, -2, id(lato), GRAY, TextAlign::CENTER_HORIZONTAL, "USB");
return {"USB"};
} else if (id(battery_voltage).state > 4.18) {
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
id(display1).image(111, -3, id(batcharge), YELLOW, BLACK);
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
if (id(battery_voltage).state > 4.5) {
id(display1).print(display_centerwidth, -2, id(lato), GREEN, TextAlign::CENTER_HORIZONTAL, "CV");
return {"Constant Voltage"};
} else {
id(display1).print(display_centerwidth, -2, id(lato), YELLOW, TextAlign::CENTER_HORIZONTAL, "CC");
return {"Constant Current"};
}
} else {
return {"Battery"};
}
Here is how Home Assistant converts this text sensor to a graph over time, providing detailed information about battery charging and discharging:
It also updates icons and status text:
- Power Mode: based on the current voltage readings, you get different icons and status texts for these modes:
- USB: board is powered by USB/5V, and not charging.
- Charging: board is charging a battery. The status differentiates between constant current (first charging phase up to 80%), and constant voltage (second charging phase from 80-100%). The display shows a yellow CC or a green CV. This allows you to disconnect the board from USB if you don’t want to fully charge the battery to conserve its life.
- Battery Power: when powered by the battery, the display shows the remaining charge in percent.
The latter - the battery state of charge, is determined by battery_percent, another templated sensor that is based on the current battery voltage:
- platform: template
name: "Battery %"
id: battery_percent
lambda: return id(battery_voltage).state;
accuracy_decimals: 0
unit_of_measurement: "%"
icon: mdi:battery-medium
entity_category: "diagnostic"
device_class: "battery"
filters:
# map battery voltages to associated SOC percentages:
- calibrate_linear:
method: exact
datapoints:
- 0.00 -> 0.0
- 3.30 -> 1.0
- 3.39 -> 10.0
- 3.75 -> 50.0
- 4.08 -> 97.0
- 4.11 -> 99.0
- 4.20 -> 100.0
# cap at 100%
- clamp:
min_value: 0
max_value: 100
ignore_out_of_range: true
# remove outliers (both directions):
- median:
window_size: 7
send_every: 4
send_first_at: 3
This Home Assistant graph shows a smooth and almost linear battery percent curve when discharging the battery:
calibrate_linear:
calibrates the state of charge via some reference points. LiIon battery voltage often drops considerably right after disconnecting them from the charger (which is called relaxation). The reference points take this into account and produce a curve that linearly depicts the true state of charge based on current battery voltage.
clamp:
limits the range to 0-100, eliminating invalid readings. And median:
filters out outliers. Since outliers can be both positive and negative, this sensor uses the median filter rather than the quantile filter (which eliminated positive spikes).
on_value:
updates the battery icon whenever a new state of charge is emitted. It also prints the current battery charge (in percent) to the screen.
Battery Icon and State of Charge
It then also serves to update the battery icon in the display which occurs every time a new battery percentage is reported:
# on new battery percentage, update text and icon on display:
on_value:
then:
# update battery percent on display:
- lambda: |-
// output battery percentage horizontally aligned:
if (id(battery_voltage).state < 4.18) {
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
// update battery percent:
id(display1).printf(display_centerwidth, -2, id(lato), LIGHTGRAY, TextAlign::CENTER_HORIZONTAL, "%.0f%%", id(battery_percent).state);
}
// show battery icon in right corner of display:
// clear icon background:
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
// read current percentage as integer:
int batPercent = (int)id(battery_percent).state;
// draw appropriate icon:
if (batPercent<5) {
id(display1).image(111, -3, id(bat1), RED, BLACK);
} else if(batPercent < 20) {
id(display1).image(111, -3, id(bat2), ORANGE, BLACK);
} else if(batPercent < 40) {
id(display1).image(111, -3, id(bat3), ORANGE, BLACK);
} else if(batPercent < 60) {
id(display1).image(111, -3, id(bat3), GREEN, BLACK);
} else if(batPercent < 80) {
id(display1).image(111, -3, id(bat4), GREEN, BLACK);
} else if(batPercent < 100) {
id(display1).image(111, -3, id(bat5), GREEN, BLACK);
} else {
id(display1).image(111, -3, id(powerPlug), YELLOW, BLACK);
}
Note how the battery state and icon is added to the device in Home Assistant so you can immediately see which of your ESPHome devices has a battery, and what the current battery status is:
Manual Deep Sleep
When the GPIO0 push button is pressed for at least 3.1s, deep sleep is manually invoked:
# GPIOs (PHYSICAL BUTTONS)
binary_sensor:
# left button (boot button):
- platform: gpio
name: "Push Button Left"
id: button_left
icon: "mdi:toggle-switch-outline"
pin:
number: GPIO0
# low active:
inverted: True
mode:
input: True
pullup: True
# debounce:
filters:
- delayed_on: 10ms
- delayed_off: 10ms
# super long press (<3s) without need to release
on_multi_click:
- timing:
- ON for at least 3.1s
then:
# invoke deep sleep
- text_sensor.template.publish:
id: sleep_state
state: "Deep Sleep (manual invoke)"
- script.execute: prepare_for_sleep
Note how the button publishes the appropriate deep sleep event to the text_sensor:
named sleep_state before it actually invokes deep sleep.
on_multi_click:
is the only way you have to respond to a pushed-down button after a given time without the need for the user to first release it. So whenever a user pushes down the button for at least 3.1s, the action invokes the deep sleep script.
Freely Programmable Buttons
All the other potential functionalities you may want to associate with this push button are handled via on_click:
:
- Internal very short push: when the user pushes the button only shortly, it controls the display backlight brightness.
- Short Press: when pushed for no longer than 0.5s, it is invoking the short press action which is also published to Home Assistant - so you can trigger automations based on this push.
- Long Press: any pushing for longer than 0.5s but no more than 3s is treated as a long push, and also published to Home Assistant.
on_click:
# very short push increases display backlight brightness:
- min_length: 50ms
max_length: 250ms
then:
- logger.log: "physically dimming up"
- light.dim_relative:
id: back_light
relative_brightness: 5%
transition_length: 0.1s
# short press:
- min_length: 251ms
max_length: 500ms
then:
- logger.log: "physical Short Press 1"
- button.press: short_press1
# long press:
- min_length: 501ms
max_length: 3000ms
then:
- logger.log: "physical Long Press 1"
- button.press: long_press1
You can use all of these events internally (within this configuration), i.e. to trigger other actions or local scripts. Currently, all events just invoke logger.log:
, and write debug messages into the log.
You can also remotely press the buttons from your Home Assistant UI. They are published to Home Assistant
via button:
. One button is virtual only and not attached to any physical button:
button:
- platform: template
name: "DeepSleep Invoke"
id: deep_sleep_button
entity_category: "diagnostic"
device_class: ""
icon: mdi:sleep
# send device to deep sleep remotely:
on_press:
then:
- text_sensor.template.publish:
id: sleep_state
state: "Deep Sleep (remote invoke)"
- script.execute: prepare_for_sleep
Home Assistant shows this button in the device user interface, and when you click it, the device enters deep sleep. All physical buttons also got virtual Home Assistant buttons, so they, too, can be remotely operated:
- platform: template
name: "Button1 ShortPress"
id: short_press1
# no device action defined yet
on_press:
then:
- logger.log: "Button1 ShortPress Pressed"
- platform: template
name: "Button1 LongPress"
id: long_press1
# no device action defined yet
on_press:
then:
- logger.log: "Button1 LongPress Pressed"
- platform: template
name: "Button2 ShortPress"
id: short_press2
# no device action defined yet
on_press:
then:
- logger.log: "Button2 ShortPress Pressed"
- platform: template
name: "Button2 LongPress"
id: long_press2
# no device action defined yet
on_press:
then:
- logger.log: "Button2 LongPress Pressed"
- platform: template
name: "Button2 SuperLongPress"
id: superlong_press2
# no device action defined yet
on_press:
then:
- logger.log: "Button2 SuperLongPress Pressed"
Buttons are one-way: Home Assistant can remotely control them, but when you operate a button locally via a physical device button, Home Assistant does not notice or log this, nor can you attach automations to a button. If you wanted to enable this, too, you’d have to publish dedicated Home Assistant events via its API.
Freely Programmable Switch
In addition, the push button can also act like a switch: for as long as the user presses the button, the switch is ON, and once the user releases the button, the switch turned OFF:
# holding down the button:
on_press:
then:
- logger.log: "physically switching ON left switch"
- switch.template.publish:
# switch is ON
id: button1_switch
state: ON
# releasing pushed down button:
on_release:
then:
- logger.log: "physically switching OFF left switch"
- switch.template.publish:
# switch is OFF
id: button1_switch
state: OFF
Switches perform two-way communications, so when you operate a switch - locally on the device, or remotely via Home Assistant - the action is logged by Home Assistant. Likewise, you can attach automations* to switches.
Display Component
The st7789v display driver component used in the original configuration is considered deprecated:
This driver is considered deprecated:
display:
- platform: st7789v
This configuration uses the modern and more general ili9xxx
driver which superseeds the old driver. It can handle uniformly many different TFT displays and drivers, including the st7789v.
This is the replacement component configured for the T-Display display:
display:
- platform: ili9xxx
model: ST7789V
id: display1
cs_pin: GPIO5
dc_pin: GPIO16
reset_pin: GPIO23
dimensions:
height: 240
width: 135
offset_height: 40
offset_width: 52
invert_colors: true
Power-Efficient Display Management
In the original configuration, the device driver used the default update_interval: 1s
and cleared its display each time ( auto_clear_enabled:
is true by default).
In essence, the lambda code had to redraw the entire display content every second, regardless of whether it needed changes or not. This wasted a lot of battery power because the microcontroller (and display controller) were kept busy for 85ms each second just to do these redundant updates.
That’s why the new configuration sets auto_clear_enabled: false
. Only those portions that actually need updates will be redrawn. Display updates can be initiated from various components, i.e. when the battery voltage changes, the battery icon gets updated.
Drawing updates to the display now requires that the code takes care of erasing this area before drawing new things:
// draw icon:
it.filled_rectangle(0, -3, 24, 24, BLACK); // erase icon area
it.image(0, -3, id(wifiOn), GREEN, BLACK); // draw new icon
Dimmable Display Backlight
The display backlight is implemented as a light:
component, and this light is tied to its GPIO 4 via an output:
:
# use a separate "light" component for dimmable display backlight management:
output:
- platform: ledc
# GPIO4 is connected to display backlight:
pin: 4
id: display_backlight_pwm
# display backlight:
light:
- platform: monochromatic
output: display_backlight_pwm
name: "Display Backlight"
id: back_light
# remember last dim state:
restore_mode: RESTORE_AND_ON
# support a pulse effect:
effects:
- pulse:
name: noWifiConnection
min_brightness: 60%
max_brightness: 80%
This way, you can turn the backlight on and off, but you can also dim it: in Home Assistant, when you click the light bulb icon in the device Controls, a dimmable light control opens, and you can set the light intensity with a slider.
At the bottom of this control, you see a drop-down list with available light effects. The configuration defines the pulsating effect noWifiConnection which is used initially when the device has not yet established a WiFi connection. With this Home Assistant control, you can turn this effect on and off manually any time you like.
# use a separate "light" component for dimmable display backlight management:
output:
- platform: ledc
# GPIO4 is connected to display backlight:
pin: 4
id: display_backlight_pwm
# display backlight:
light:
- platform: monochromatic
output: display_backlight_pwm
name: "Display Backlight"
id: back_light
# remember last dim state:
restore_mode: RESTORE_AND_ON
# support a pulse effect:
effects:
- pulse:
name: noWifiConnection
min_brightness: 60%
max_brightness: 80%
restore_mode: RESTORE_AND_ON
makes sure your last dim level is stored and used as future default.
Sample Configuration
The configuration below is ready for copy & paste. It is not just much more power-efficient than the intial version. It has also all the built-in functionality required by a battery-operated device:
- Status bar: a small status bar shows a WiFI and a battery icon, indicating connectivity and power status. A horizontal bar shows the reception strength for WiFI which makes it easy to place the device in a spot with good WiFi coverage. In-between both icons, the current battery charge status is displayed in percent (unless connected to USB power).
- Display Dimming: pressing either one of the two buttons quickly controls display brightness. The selected brightness is stored as your new preference.
- Deep Sleep: When holding the GPIO0 (boot) button for longer than 3s, the device enters deep sleep in the most efficient mode, consuming only 370uA. Pressing the other button wakes the device again.
- Home Assistant: in Home Assistant, diagnostic sensors report battery voltage, current source of power, state of charge in percent, and WiFi reception both in dBm and percent. Both push buttons are exposed in a number of ways: as switches, short-, long-, and superlong-press buttons. And a dedicated deep sleep button lets you invoke deep sleep remotely from Home Assistant.
Configuration
Here is the ready to use complete ESPHome configuration:
# PREFERENCES and GLOBAL VARIABLES:
# save state changes to flash within 10s (i.e. new display backlight dim level)
preferences:
flash_write_interval: 10s
globals:
- id: isOnline
type: bool
restore_value: no
initial_value: 'true' # offline at first, this triggers refresh
- id: lastSignalStrength # last WiFi signal strength
type: uint
restore_value: no
initial_value: "-1"
# HARDWARE SENSORS
sensor:
# retrieve current WiFi signal strength in dBm:
- platform: wifi_signal
name: "WiFi Signal dB"
id: wifi_signal_db
#update_interval: 60s
# retrieve current WiFi signal strength in percent:
- platform: copy
source_id: wifi_signal_db
name: "WiFi Signal Percent"
id: wifi_signal_percent
icon: mdi:cloud-percent
filters:
- lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
unit_of_measurement: "Signal %"
entity_category: "diagnostic"
device_class: ""
# battery voltage sensor
- platform: adc
pin: GPIO34
id: battery_voltage
name: "Voltage"
update_interval: 1s
#internal: true
accuracy_decimals: 2
attenuation: 12dB
samples: 10
entity_category: "diagnostic"
device_class: "voltage"
filters:
# transform the raw value (voltage divider) to actual voltage:
- multiply: 2.04
# remove outliers (voltage spikes only):
- quantile:
window_size: 7
send_every: 4
send_first_at: 3
quantile: .25
# on new voltage readings, update battery statistics:
on_value:
then:
- component.update: battery_percent
- component.update: battery_status
# automatically enter deep-sleep below 3.2V:
on_value_range:
below: 3.2
then:
- text_sensor.template.publish:
id: sleep_state
state: "Sleeping (low voltage)"
- script.execute: prepare_for_sleep
# publish battery voltage in arbitrary intervals (when ADC sensor is set to internal)
#- platform: template
# name: "Battery Voltage"
# id: battery_voltage_public
# update_interval: 60s
# accuracy_decimals: 2
# lambda: return id(battery_voltage).state;
# unit_of_measurement: "V"
# device_class: voltage
# state_class: measurement
# entity_category: "diagnostic"
# publish battery state of charge in percent
- platform: template
name: "Battery %"
id: battery_percent
lambda: return id(battery_voltage).state;
accuracy_decimals: 0
unit_of_measurement: "%"
icon: mdi:battery-medium
entity_category: "diagnostic"
device_class: "battery"
filters:
# map battery voltages to associated SOC percentages:
- calibrate_linear:
method: exact
datapoints:
- 0.00 -> 0.0
- 3.30 -> 1.0
- 3.39 -> 10.0
- 3.75 -> 50.0
- 4.08 -> 97.0
- 4.11 -> 99.0
- 4.20 -> 100.0
# cap at 100%
- clamp:
min_value: 0
max_value: 100
ignore_out_of_range: true
# remove outliers (both directions):
- median:
window_size: 7
send_every: 4
send_first_at: 3
# on new battery percentage, update text and icon on display:
on_value:
then:
# update battery percent on display:
- lambda: |-
// output battery percentage horizontally aligned:
if (id(battery_voltage).state < 4.18) {
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
// update battery percent:
id(display1).printf(display_centerwidth, -2, id(lato), LIGHTGRAY, TextAlign::CENTER_HORIZONTAL, "%.0f%%", id(battery_percent).state);
}
// show battery icon in right corner of display:
// clear icon background:
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
// read current percentage as integer:
int batPercent = (int)id(battery_percent).state;
// draw appropriate icon:
if (batPercent<5) {
id(display1).image(111, -3, id(bat1), RED, BLACK);
} else if(batPercent < 20) {
id(display1).image(111, -3, id(bat2), ORANGE, BLACK);
} else if(batPercent < 40) {
id(display1).image(111, -3, id(bat3), ORANGE, BLACK);
} else if(batPercent < 60) {
id(display1).image(111, -3, id(bat3), GREEN, BLACK);
} else if(batPercent < 80) {
id(display1).image(111, -3, id(bat4), GREEN, BLACK);
} else if(batPercent < 100) {
id(display1).image(111, -3, id(bat5), GREEN, BLACK);
} else {
id(display1).image(111, -3, id(powerPlug), YELLOW, BLACK);
}
text_sensor:
# log power mode: USB or battery
- platform: template
name: "Battery Status"
id: battery_status
entity_category: "diagnostic"
device_class: ""
icon: mdi:power-plug-battery
lambda: |-
if (id(battery_voltage).state > 4.74) {
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
id(display1).image(111, -3, id(powerPlug), YELLOW, BLACK);
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
id(display1).print(display_centerwidth, -2, id(lato), GRAY, TextAlign::CENTER_HORIZONTAL, "USB");
return {"USB"};
} else if (id(battery_voltage).state > 4.18) {
id(display1).filled_rectangle(111, -3, 24, 24, BLACK);
id(display1).image(111, -3, id(batcharge), YELLOW, BLACK);
// clear text background:
id(display1).filled_rectangle(40, -2, 55, 20, BLACK);
// output text center-aligned horizontally
int display_centerwidth = id(display1).get_width()/2;
if (id(battery_voltage).state > 4.5) {
id(display1).print(display_centerwidth, -2, id(lato), GREEN, TextAlign::CENTER_HORIZONTAL, "CV");
return {"Constant Voltage"};
} else {
id(display1).print(display_centerwidth, -2, id(lato), YELLOW, TextAlign::CENTER_HORIZONTAL, "CC");
return {"Constant Current"};
}
} else {
return {"Battery"};
}
# log system state: running or deep sleep (and reason)
- platform: template
name: "DeepSleep State"
id: sleep_state
icon: "mdi:power-sleep"
entity_category: "diagnostic"
device_class: ""
# DEEP SLEEP
# enable deep sleep capabilities and set wakeup-pin in energy-efficient ext1 mode
deep_sleep:
id: deep_sleep_control
esp32_ext1_wakeup: # uses much less deep sleep power than wakeup_pin:
pins:
- number: 35
allow_other_uses: true
mode: ALL_LOW
# perform all necessary actions to send peripherals to deep sleep
script:
- id: prepare_for_sleep
then:
# switch display controller in sleep mode:
- lambda: |-
// Send display to sleep before deep sleep
uint8_t command = 0x10; // Your command
id(display1).command(command);
- delay: 120ms
# send esp32 to deep sleep:
- deep_sleep.enter: deep_sleep_control
# PUSH BUTTONS that can be pushed physically and remotely via Home Assistant
button:
- platform: template
name: "DeepSleep Invoke"
id: deep_sleep_button
entity_category: "diagnostic"
device_class: ""
icon: mdi:sleep
# send device to deep sleep remotely:
on_press:
then:
- text_sensor.template.publish:
id: sleep_state
state: "Sleeping (remote invoke)"
- script.execute: prepare_for_sleep
- platform: template
name: "WiFi Measure Strength"
id: update_wifi
entity_category: "diagnostic"
device_class: ""
icon: mdi:wifi-sync
# manually update wifi signal strength
on_press:
then:
- sensor.template.publish:
id: wifi_signal_db
state: !lambda 'return id(wifi_signal_db).state;'
- platform: template
name: "Button1 ShortPress"
id: short_press1
# no device action defined yet
on_press:
then:
- logger.log: "Button1 ShortPress Pressed"
- platform: template
name: "Button1 LongPress"
id: long_press1
# no device action defined yet
on_press:
then:
- logger.log: "Button1 LongPress Pressed"
- platform: template
name: "Button2 ShortPress"
id: short_press2
# no device action defined yet
on_press:
then:
- logger.log: "Button2 ShortPress Pressed"
- platform: template
name: "Button2 LongPress"
id: long_press2
# no device action defined yet
on_press:
then:
- logger.log: "Button2 LongPress Pressed"
# ON/OFF SWITCHES that can be switched physically and remotely via Home Assistant:
switch:
# ON while device button 1 is kept pressed down
- platform: template
name: "Button1 Switch"
restore_mode: ALWAYS_OFF
id: button1_switch
optimistic: true
icon: "mdi:toggle-switch-outline"
# ON while device button 1 is kept pressed down
- platform: template
name: "Button2 Switch"
restore_mode: ALWAYS_OFF
id: button2_switch
icon: "mdi:toggle-switch-outline"
optimistic: true
binary_sensor:
# Home Assistant API connectivity
- platform: status
name: "API Home Assistant"
id: api_status
on_state:
then:
- if:
condition:
lambda: 'return id(api_status).state;'
# if Home Assistant API is connected, the board is running:
then:
- text_sensor.template.publish:
id: sleep_state
state: "Running"
# if the API is not in reach, the board is still running:
else:
- text_sensor.template.publish:
id: sleep_state
state: "Running"
# physical GPIOs (buttons)
# left button (boot button):
- platform: gpio
name: "Button Left"
id: button_left
icon: "mdi:toggle-switch-outline"
pin:
number: GPIO0
# low active:
inverted: True
mode:
input: True
pullup: True
# debounce:
filters:
- delayed_on: 10ms
- delayed_off: 10ms
on_click:
# very short push increases display backlight brightness:
- min_length: 50ms
max_length: 250ms
then:
- logger.log: "physically dimming up"
- light.dim_relative:
id: back_light
relative_brightness: 5%
transition_length: 0.1s
# short press:
- min_length: 251ms
max_length: 500ms
then:
- logger.log: "physical Short Press 1"
- button.press: short_press1
# long press:
- min_length: 501ms
max_length: 3000ms
then:
- logger.log: "physical Long Press 1"
- button.press: long_press1
# super long press (<3s) without need to release
on_multi_click:
- timing:
- ON for at least 3.1s
then:
# invoke deep sleep
- text_sensor.template.publish:
id: sleep_state
state: "Sleeping (manual invoke)"
- script.execute: prepare_for_sleep
# holding down the button:
on_press:
then:
- logger.log: "physically switching ON left switch"
- switch.template.publish:
# switch is ON
id: button1_switch
state: ON
# releasing pushed down button:
on_release:
then:
- logger.log: "physically switching OFF left switch"
- switch.template.publish:
# switch is OFF
id: button1_switch
state: OFF
# right button
- platform: gpio
name: 'Button Right'
id: button_right
icon: "mdi:toggle-switch-outline"
pin:
number: GPIO35
# low active:
inverted: True
# is also used as deep-sleep wakeup pin:
allow_other_uses: true
mode:
input: True
# debounce:
filters:
- delayed_on: 10ms
- delayed_off: 10ms
# very short push decreases display backlight brightness:
on_click:
- min_length: 50ms
max_length: 250ms
then:
- logger.log: "physically dimming down"
- light.dim_relative:
id: back_light
relative_brightness: -5%
transition_length: 0.1s
# short press:
- min_length: 251ms
max_length: 500ms
then:
- logger.log: "physical Short Press 2"
- button.press: short_press2
# long press:
- min_length: 501ms
max_length: 3000ms
then:
- logger.log: "physical Long Press 2"
- button.press: long_press2
# super long press (<3s) without need to release
on_multi_click:
- timing:
- ON for at least 3.1s
then:
# update wifi strength sensor:
- logger.log: "updating WiFi strength measurement"
- button.press: update_wifi
# holding down the button:
on_press:
then:
- logger.log: "physically switching ON right switch"
- switch.template.publish:
# switch is ON
id: button2_switch
state: ON
# releasing pushed down button:
on_release:
then:
- logger.log: "physically switching OFF right switch"
- switch.template.publish:
# switch is OFF
id: button2_switch
state: OFF
# DISPLAY
# use a separate "light" component for dimmable display backlight management:
output:
- platform: ledc
# GPIO4 is connected to display backlight:
pin: 4
id: display_backlight_pwm
# display backlight:
light:
- platform: monochromatic
output: display_backlight_pwm
name: "Display Backlight"
id: back_light
# remember last dim state:
restore_mode: RESTORE_AND_ON
# support a pulse effect:
effects:
- pulse:
name: noWifiConnection
min_brightness: 60%
max_brightness: 80%
# internal SPI for display:
spi:
clk_pin: GPIO18
mosi_pin: GPIO19
id: spi_bus
# display component (dedicated st7789 component is deprecated, use generic ili9xxx instead):
display:
- platform: ili9xxx
model: ST7789V
id: display1
# hardware pins:
cs_pin: GPIO5
dc_pin: GPIO16
reset_pin: GPIO23
# T-Display screen dimensions:
dimensions:
height: 240
width: 135
offset_height: 40
offset_width: 52
# T-Display requires inverted codes:
invert_colors: true
# do not auto-clear. Redraw only when needed.
auto_clear_enabled: false
update_interval: 1s
# rotation: 90°
lambda: |-
// get horizontal display width:
uint displayWidth = it.get_width();
// are we currently connected to WiFi?
if (WiFi.status() == WL_CONNECTED) {
// did we just come online and were offline before?
if (id(isOnline) == false) {
// disable display backlight pulse:
id(back_light).turn_on().set_effect("none").perform();
// draw icon:
it.filled_rectangle(0, -3, 24, 24, BLACK); // erase icon area (for icons with transparency)
it.image(0, -3, id(wifiOn), GREEN, BLACK); // use black background (for icons w/o transparency)
// update state change:
id(isOnline) = true;
}
// draw signal strength bar only when connected to WiFi
uint signal_strength = id(wifi_signal_percent).state; // get the current signal strength
if (id(lastSignalStrength) != signal_strength)
{
id(lastSignalStrength) = signal_strength;
uint bar_length = signal_strength * displayWidth / 100; // map signal strength to bar length
// erase bar background:
it.filled_rectangle(0, 24, displayWidth, 2, GRAY);
// draw the bar:
it.filled_rectangle(0, 24, bar_length, 2, GREEN);
}
} else {
// did we just go offline and were online before?
// this also applies to boot where global variable isOnline is preset to true
if (id(isOnline) == true) {
// enable display backlight pulse:
id(back_light).turn_on().set_effect("noWifiConnection").perform();
// draw icon
it.filled_rectangle(0, -3, 24, 24, BLACK); // erase icon area (for icons with transparency)
it.image(0, -3, id(wifiOn), GRAY, BLACK);
// erase wifi strength graph:
it.filled_rectangle(0, 24, displayWidth, 2, GRAY);
id(lastSignalStrength) = 0;
// update state change:
id(isOnline) = false;
}
}
font:
- file:
type: gfonts
family: Lato
weight: 400
id: lato
size: 20
- file:
type: gfonts
family: Lato
weight: 700
id: latobold
size: 24
color:
- id: WHITE
red: 100%
green: 100%
blue: 100%
white: 100%
- id: RED
red: 100%
green: 0%
blue: 0%
white: 0%
- id: GREEN
red: 0%
green: 100%
blue: 0%
white: 0%
- id: BLUE
red: 0%
green: 0%
blue: 100%
white: 0%
- id: YELLOW
red: 100%
green: 100%
blue: 0%
white: 0%
- id: ORANGE
red: 100%
green: 65%
blue: 0%
white: 0%
- id: BLACK
red: 0%
green: 0%
blue: 0%
white: 0%
- id: GRAY
red: 10%
green: 10%
blue: 10%
white: 0%
- id: LIGHTGRAY
red: 80%
green: 80%
blue: 80%
white: 0%
- id: MEDIUMGRAY
red: 50%
green: 50%
blue: 50%
white: 0%
image:
- file: mdi:wifi
id: wifiOn
resize: 24x24
- file: mdi:wifi-off
id: wifiOff
resize: 24x24
- file: mdi:battery-off-outline
id: bat1
resize: 24x24
- file: mdi:battery-outline
id: bat2
resize: 24x24
- file: mdi:battery-low
id: bat3
resize: 24x24
- file: mdi:battery-medium
id: bat4
resize: 24x24
- file: mdi:battery-high
id: bat5
resize: 24x24
- file: mdi:flash
id: batcharge
resize: 24x24
- file: mdi:power-plug
id: powerPlug
resize: 24x24
Slow Website?
This website is very fast, and pages should appear instantly. If this site is slow for you, then your routing may be messed up, and this issue does not only affect done.land, but potentially a few other websites and downloads as well. Here are simple steps to speed up your Internet experience and fix issues with slow websites and downloads..
Comments
Please do leave comments below. I am using utteran.ce, an open-source and ad-free light-weight commenting system.
Here is how your comments are stored
Whenever you leave a comment, a new github issue is created on your behalf.
-
All comments become trackable issues in the Github Issues section, and I (and you) can follow up on them.
-
There is no third-party provider, no disrupting ads, and everything remains transparent inside github.
Github Users Yes, Spammers No
To keep spammers out and comments attributable, all you do is log in using your (free) github account and grant utteranc.es the permission to submit issues on your behalf.
If you don’t have a github account yet, go get yourself one - it’s free and simple.
If for any reason you do not feel comfortable with letting the commenting system submit issues for you, then visit Github Issues directly, i.e. by clicking the red button Submit Issue at the bottom of each page, and submit your issue manually. You control everything.
Discussions
For chit-chat and quick questions, feel free to visit and participate in Discussions. They work much like classic forums or bulletin boards. Just keep in mind: your valued input isn’t equally well trackable there.
(content created Oct 06, 2024 - last updated Oct 09, 2024)