-
-
Save kmplngj/c02d0f3e0d68ad97dc4c2fcd3a0edb51 to your computer and use it in GitHub Desktop.
# This ESPHome configuration file is for a GeekMagic Display device (esp01). | |
# It sets up the device to display pages of text and notifications, | |
# with customizable intervals and padding. The display is updated | |
# periodically and can show different pages based on Home Assistant sensors. | |
# The configuration includes: | |
# - WiFi setup with fallback hotspot | |
# - Web server for remote access | |
# Usage: | |
# - The device connects to your WiFi network and can be accessed via a web server. | |
# - It displays text from Home Assistant sensors on a rotating basis. | |
# - Notifications can be sent to the display via the Home Assistant API. | |
# - Display update and page change intervals can be customized. | |
# - Padding can be adjusted. | |
# - The display can be turned on and off remotely. Brightness can be adjusted. | |
# - The page rotation can be enabled or disabled. | |
# - The display can be set to a fixed page. | |
# - The display can show different colors and font sizes with special markers. | |
# - Progress bars can be displayed at the top and bottom of the screen. | |
# Possible markers for text customization: | |
# Color theme is Catppuccino Macchiato | |
# - [rosewater]: Changes the text color to rosewater. | |
# - [flamingo]: Changes the text color to flamingo. | |
# - [pink]: Changes the text color to pink. | |
# - [mauve]: Changes the text color to mauve. | |
# - [red]: Changes the text color to red. | |
# - [maroon]: Changes the text color to maroon. | |
# - [peach]: Changes the text color to peach. | |
# - [yellow]: Changes the text color to yellow. | |
# - [green]: Changes the text color to green. | |
# - [teal]: Changes the text color to teal. | |
# - [sky]: Changes the text color to sky. | |
# - [sapphire]: Changes the text color to sapphire. | |
# - [blue]: Changes the text color to blue. | |
# - [lavender]: Changes the text color to lavender. | |
# - [text]: Changes the text color to text. | |
# - [subtext1]: Changes the text color to subtext1. | |
# - [subtext0]: Changes the text color to subtext0. | |
# - [overlay2]: Changes the text color to overlay2. | |
# - [overlay1]: Changes the text color to overlay1. | |
# - [overlay0]: Changes the text color to overlay0. | |
# - [surface2]: Changes the text color to surface2. | |
# - [surface1]: Changes the text color to surface1. | |
# - [surface0]: Changes the text color to surface0. | |
# - [base]: Changes the text color to base. | |
# - [mantle]: Changes the text color to mantle. | |
# - [crust]: Changes the text color to crust. | |
# - [center]: Centers the text. | |
# - [left]: Aligns the text to the left. | |
# - [right]: Aligns the text to the right. | |
# - [font_small]: Changes the font size to small. | |
# - [bg_rosewater]: Changes the background color to rosewater. | |
# - [bg_flamingo]: Changes the background color to flamingo. | |
# - [bg_pink]: Changes the background color to pink. | |
# - [bg_mauve]: Changes the background color to mauve. | |
# - [bg_red]: Changes the background color to red. | |
# - [bg_maroon]: Changes the background color to maroon. | |
# - [bg_peach]: Changes the background color to peach. | |
# - [bg_yellow]: Changes the background color to yellow. | |
# - [bg_green]: Changes the background color to green. | |
# - [bg_teal]: Changes the background color to teal. | |
# - [bg_sky]: Changes the background color to sky. | |
# - [bg_sapphire]: Changes the background color to sapphire. | |
# - [bg_blue]: Changes the background color to blue. | |
# - [bg_lavender]: Changes the background color to lavender. | |
# - [bg_text]: Changes the background color to text. | |
# - [bg_subtext1]: Changes the background color to subtext1. | |
# - [bg_subtext0]: Changes the background color to subtext0. | |
# - [bg_overlay2]: Changes the background color to overlay2. | |
# - [bg_overlay1]: Changes the background color to overlay1. | |
# - [bg_overlay0]: Changes the background color to overlay0. | |
# - [bg_surface2]: Changes the background color to surface2. | |
# - [bg_surface1]: Changes the background color to surface1. | |
# - [bg_surface0]: Changes the background color to surface0. | |
# - [bg_base]: Changes the background color to base. | |
# - [bg_mantle]: Changes the background color to mantle. | |
# - [bg_crust]: Changes the background color to crust. | |
# - [column_1]: Starts the first column. | |
# - [column_2]: Starts the second column. | |
# - [initial] [/initial]: Sets the page initial (max 4 characters). | |
# - "\n": Adds a new line. | |
# Here is an example string that can be displayed: | |
# "[bg_crust][center]Welcome to Page 1[/center]\n[red]Important: Check the status below.[/red]\n[column_1][font_small]Column 1 Text[/font_small][/column_1][column_2][font_small]Column 2 Text[/font_small][/column_2]\nEnjoy your stay!\n[green][/green]\n[blue]Blue Text[/blue][/bg_crust]" | |
# Here is an example string that can be displayed with columns: | |
# "[column_1][font_small][red]This is text in column 1.\n[column_2]This is text in column 2.\n" | |
# The display can be controlled via the following Home Assistant sensors: | |
# This uses the substitutions friendly_sensor_name value to create the sensor names. Defaults would be | |
# - sensor.geekmagic_display_07c9f4_page_1 | |
# - sensor.geekmagic_display_07c9f4_page_2 | |
# - sensor.geekmagic_display_07c9f4_page_3 | |
# - sensor.geekmagic_display_07c9f4_page_4 | |
# - sensor.geekmagic_display_07c9f4_page_5 | |
# - number.geekmagic_display_07c9f4_progress_top | |
# - number.geekmagic_display_07c9f4_progress_bottom | |
# These sensors can be updated via Home Assistant automations, scripts, templates. | |
# The display can be controlled via the following Home Assistant services: | |
# This service can be called via Home Assistant automations, scripts, templates. | |
# - esphome.display_notification | |
# The service requires the following parameters: | |
# - title: string | |
# - message: string | |
# - timeout: int | |
# - esphome.trigger_display_update | |
# - esphome.show_specific_page | |
# The service requires the following parameter: | |
# - page_number: int | |
# - esphome.deactivate_notification | |
# - esphome.repeat_last_notification | |
# - esphome.next_page | |
# - esphome.previous_page | |
# - esphome.jump_to_page | |
# The service requires the following parameter: | |
# - page_number: int | |
# - esphome.set_number_of_pages | |
# The service requires the following parameter: | |
# - num_pages: int | |
substitutions: | |
device_name: esphome-web-07c9f4 | |
friendly_sensor_name: geekmagic_display_07c9f4 | |
esphome: | |
name: ${device_name} | |
friendly_name: GeekMagic Display | |
min_version: 2024.11.0 | |
name_add_mac_suffix: false | |
on_boot: | |
priority: 10 | |
then: | |
- script.execute: initialize_device | |
esp8266: | |
board: esp01_1m | |
external_components: | |
- source: | |
type: git | |
url: https://github.com/rletendu/esphome.git | |
ref: st7789_nobuffer_202312 | |
components: [st7789v] | |
# logger: | |
# level: NONE # Reduced logging level to save memory! | |
api: | |
encryption: | |
key: !secret api-key | |
actions: | |
- action: display_notification | |
variables: | |
message: string | |
timeout: int | |
then: | |
- lambda: |- | |
id(notification_message) = message; | |
id(notification_timeout) = timeout; | |
- script.execute: show_notification | |
- action: trigger_display_update | |
then: | |
- script.execute: update_display | |
- action: show_specific_page | |
variables: | |
page_number: int | |
then: | |
- lambda: |- | |
id(page) = page_number; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: deactivate_notification | |
then: | |
- lambda: |- | |
id(notification_active) = false; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: repeat_last_notification | |
then: | |
- lambda: |- | |
id(notification_active) = true; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- delay: !lambda "return id(notification_timeout) * 1000;" | |
- lambda: |- | |
id(notification_active) = false; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: next_page | |
then: | |
- lambda: |- | |
id(page) = (id(page) % id(number_of_pages)) + 1; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: previous_page | |
then: | |
- lambda: |- | |
id(page) = (id(page) - 2 + id(number_of_pages)) % id(number_of_pages) + 1; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: jump_to_page | |
variables: | |
page_number: int | |
then: | |
- lambda: |- | |
id(page) = page_number; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- action: set_number_of_pages | |
variables: | |
num_pages: int | |
then: | |
- lambda: |- | |
id(number_of_pages) = num_pages; | |
- action: set_page_initial | |
variables: | |
initial: string | |
then: | |
- lambda: |- | |
id(page_initial) = initial.substr(0, 4); // Limit to 4 characters | |
ota: | |
- platform: esphome | |
safe_mode: | |
num_attempts: 5 # Number of failed boot attempts before entering safe mode | |
reboot_timeout: 10s # Time to wait before rebooting in safe mode | |
# on_safe_mode: | |
# then: | |
# - logger.log: "Entering safe mode due to repeated boot failures" | |
wifi: | |
ssid: !secret wifi-ssid | |
password: !secret wifi-password | |
ap: | |
ssid: ${device_name}_hotspot | |
password: !secret wifi-recovery-password | |
web_server: | |
port: 80 | |
version: 3 | |
auth: | |
username: !secret server_username | |
password: !secret server_password | |
sorting_groups: | |
- id: sorting_group_display_settings | |
name: "Display Settings" | |
sorting_weight: 20 | |
- id: sorting_group_network_settings | |
name: "Network Settings" | |
sorting_weight: 30 | |
- id: sorting_group_control_buttons | |
name: "Control Buttons" | |
sorting_weight: 10 | |
- id: sorting_group_control_buttons_basic | |
name: "ESPHome Basic Control Buttons" | |
sorting_weight: 25 | |
captive_portal: | |
button: | |
# The restart button platform allows you to restart your node remotely through Home Assistant. | |
# https://esphome.io/components/button/restart.html | |
- platform: restart | |
name: ${device_name} Restart | |
web_server: | |
sorting_group_id: sorting_group_control_buttons_basic | |
# The safe_mode button allows you to remotely reboot your node into Safe Mode. | |
# https://esphome.io/components/button/safe_mode.html | |
- platform: safe_mode | |
name: ${device_name} Restart (Safe Mode) | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Deactivate Notification" | |
id: deactivate_notification_button | |
on_press: | |
then: | |
- lambda: |- | |
id(notification_active) = false; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons_basic | |
- platform: template | |
name: "Update Display" | |
id: update_display_button | |
on_press: | |
then: | |
- script.execute: update_display | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Next Page" | |
id: next_page_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = (id(page) % id(number_of_pages)) + 1; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Previous Page" | |
id: previous_page_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = (id(page) - 2 + id(number_of_pages)) % id(number_of_pages) + 1; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Show Last Notification" | |
id: show_last_notification_button | |
on_press: | |
then: | |
- script.execute: show_notification | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Jump to Page 1" | |
id: jump_to_page_1_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = 1; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Jump to Page 2" | |
id: jump_to_page_2_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = 2; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Jump to Page 3" | |
id: jump_to_page_3_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = 3; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Jump to Page 4" | |
id: jump_to_page_4_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = 4; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Jump to Page 5" | |
id: jump_to_page_5_button | |
on_press: | |
then: | |
- lambda: |- | |
id(page) = 5; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
- platform: template | |
name: "Display Buzzer" | |
id: display_buzzer_button | |
on_press: | |
then: | |
- script.execute: display_buzzer | |
web_server: | |
sorting_group_id: sorting_group_control_buttons | |
switch: | |
- platform: template | |
name: "Enable Page Rotation" | |
id: page_rotation_switch | |
optimistic: true | |
restore_mode: RESTORE_DEFAULT_OFF | |
on_turn_on: | |
then: | |
- script.execute: update_display | |
on_turn_off: | |
then: | |
- script.execute: update_display | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: template | |
name: "Enable Auto Display Update" | |
id: auto_display_update_switch | |
optimistic: true | |
restore_mode: RESTORE_DEFAULT_OFF | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
spi: | |
clk_pin: GPIO14 | |
mosi_pin: GPIO13 | |
interface: hardware | |
id: spihwd | |
time: | |
- platform: homeassistant | |
id: homeassistant_time | |
output: | |
- platform: esp8266_pwm | |
pin: GPIO05 | |
frequency: 40 Hz | |
id: pwm_output | |
inverted: true | |
light: | |
- platform: monochromatic | |
output: pwm_output | |
name: "Backlight" | |
id: back_light | |
restore_mode: RESTORE_DEFAULT_ON | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
# - platform: status_led | |
# name: "Status LED" | |
# pin: GPIO10 | |
font: | |
- file: | |
type: gfonts | |
family: Inter Tight | |
weight: 400 | |
id: font_normal | |
size: 21 | |
glyphs: '!"%()+,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/' | |
bpp: 2 | |
- file: | |
type: gfonts | |
family: Inter Tight | |
weight: 400 | |
id: font_small | |
size: 17 | |
glyphs: '!"%()+,-_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz/' | |
bpp: 2 | |
color: | |
- id: color_rosewater | |
hex: f4dbd6 | |
- id: color_flamingo | |
hex: f0c6c6 | |
- id: color_pink | |
hex: f5bde6 | |
- id: color_mauve | |
hex: c6a0f6 | |
- id: color_red | |
hex: ed8796 | |
- id: color_maroon | |
hex: ee99a0 | |
- id: color_peach | |
hex: f5a97f | |
- id: color_yellow | |
hex: eed49f | |
- id: color_green | |
hex: a6da95 | |
- id: color_teal | |
hex: 8bd5ca | |
- id: color_sky | |
hex: 91d7e3 | |
- id: color_sapphire | |
hex: 7dc4e4 | |
- id: color_blue | |
hex: 8aadf4 | |
- id: color_lavender | |
hex: b7bdf8 | |
- id: color_text | |
hex: cad3f5 | |
- id: color_subtext1 | |
hex: b8c0e0 | |
- id: color_subtext0 | |
hex: a5adcb | |
- id: color_overlay2 | |
hex: 939ab7 | |
- id: color_overlay1 | |
hex: 8087a2 | |
- id: color_overlay0 | |
hex: 6e738d | |
- id: color_surface2 | |
hex: 5b6078 | |
- id: color_surface1 | |
hex: 494d64 | |
- id: color_surface0 | |
hex: 363a4f | |
- id: color_base | |
hex: 24273a | |
- id: color_mantle | |
hex: 1e2030 | |
- id: color_crust | |
hex: 181926 | |
# sensor: | |
# # Uptime sensor | |
# - platform: uptime | |
# name: ${device_name}_uptime | |
# # WiFi Signal sensor | |
# - platform: wifi_signal | |
# name: ${device_name}_wifi_signal | |
# update_interval: 30s | |
text_sensor: | |
- platform: homeassistant | |
id: geekmagic_page_1 | |
entity_id: sensor.${friendly_sensor_name}_page_1 | |
on_value: | |
- if: | |
condition: | |
lambda: "return id(auto_display_update_switch).state && id(page) == 1;" | |
then: | |
- script.execute: update_display | |
- platform: homeassistant | |
id: geekmagic_page_2 | |
entity_id: sensor.${friendly_sensor_name}_page_2 | |
on_value: | |
then: | |
- if: | |
condition: | |
lambda: "return id(auto_display_update_switch).state && id(page) == 2;" | |
then: | |
- script.execute: update_display | |
- platform: homeassistant | |
id: geekmagic_page_3 | |
entity_id: sensor.${friendly_sensor_name}_page_3 | |
on_value: | |
then: | |
- if: | |
condition: | |
lambda: "return id(auto_display_update_switch).state && id(page) == 3;" | |
then: | |
- script.execute: update_display | |
- platform: homeassistant | |
id: geekmagic_page_4 | |
entity_id: sensor.${friendly_sensor_name}_page_4 | |
on_value: | |
then: | |
- if: | |
condition: | |
lambda: "return id(auto_display_update_switch).state && id(page) == 4;" | |
then: | |
- script.execute: update_display | |
- platform: homeassistant | |
id: geekmagic_page_5 | |
entity_id: sensor.${friendly_sensor_name}_page_5 | |
on_value: | |
then: | |
- if: | |
condition: | |
lambda: "return id(auto_display_update_switch).state && id(page) == 5;" | |
then: | |
- script.execute: update_display | |
- platform: version | |
name: ${device_name}_version | |
- platform: wifi_info | |
ip_address: | |
name: ${device_name}_ip | |
ssid: | |
name: ${device_name}_ssid | |
globals: | |
- id: page | |
type: int | |
initial_value: "1" | |
restore_value: yes | |
- id: notification_active | |
type: bool | |
initial_value: "false" | |
restore_value: yes | |
- id: notification_message | |
type: std::string | |
initial_value: '"Reset Notification."' | |
restore_value: yes | |
- id: notification_timeout | |
type: int | |
initial_value: "2" | |
restore_value: yes | |
- id: display_update_triggered | |
type: bool | |
initial_value: "false" | |
restore_value: yes | |
- id: background_color | |
type: Color | |
initial_value: "color_base" | |
restore_value: yes | |
- id: last_background_color | |
type: Color | |
initial_value: "color_base" | |
restore_value: yes | |
- id: first_boot | |
type: bool | |
initial_value: "true" | |
restore_value: yes | |
- id: startup_message_shown | |
type: bool | |
initial_value: "false" | |
restore_value: yes | |
- id: progress_top | |
type: int | |
initial_value: "0" | |
restore_value: yes | |
- id: progress_bottom | |
type: int | |
initial_value: "0" | |
restore_value: yes | |
- id: current_brightness | |
type: float | |
initial_value: "0.0" | |
restore_value: yes | |
- id: number_of_pages | |
type: int | |
initial_value: "3" | |
restore_value: yes | |
- id: page_initial | |
type: std::string | |
initial_value: '""' | |
restore_value: yes | |
number: | |
- platform: template | |
name: "Display Update Interval" | |
id: display_update_interval | |
min_value: 1 | |
max_value: 120 | |
step: 1 | |
unit_of_measurement: "s" | |
initial_value: 30 | |
optimistic: true | |
restore_value: yes | |
on_value: | |
then: | |
- script.execute: update_display | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: template | |
name: "Page Change Interval" | |
id: page_change_interval | |
min_value: 1 | |
max_value: 120 | |
step: 1 | |
unit_of_measurement: "s" | |
initial_value: 10 | |
optimistic: true | |
restore_value: yes | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: template | |
name: "Fixed Page" | |
id: fixed_page | |
min_value: 1 | |
max_value: 5 | |
step: 1 | |
unit_of_measurement: "" | |
initial_value: 1 | |
optimistic: true | |
restore_value: yes | |
on_value: | |
then: | |
- lambda: |- | |
id(page) = id(fixed_page).state; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: template | |
name: "Left/Right Padding" | |
id: lr_padding | |
min_value: 0 | |
max_value: 15 | |
step: 1 | |
unit_of_measurement: "px" | |
initial_value: 5 | |
optimistic: true | |
restore_value: yes | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
sorting_weight: 100 | |
- platform: template | |
name: "Top/Bottom Padding" | |
id: tb_padding | |
min_value: 0 | |
max_value: 15 | |
step: 1 | |
unit_of_measurement: "px" | |
initial_value: 10 | |
optimistic: true | |
restore_value: yes | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
sorting_weight: 110 | |
- platform: template | |
name: "Number of Pages to Rotate" | |
id: number_of_pages_template | |
min_value: 2 | |
max_value: 5 | |
step: 1 | |
unit_of_measurement: "" | |
initial_value: 3 | |
optimistic: true | |
restore_value: yes | |
on_value: | |
then: | |
- lambda: |- | |
id(number_of_pages) = int(id(number_of_pages_template).state); | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: homeassistant | |
id: progress_top_sensor | |
entity_id: number.${friendly_sensor_name}_progress_top | |
on_value: | |
then: | |
- lambda: |- | |
id(progress_top) = int(id(progress_top_sensor).state); | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
- platform: homeassistant | |
id: progress_bottom_sensor | |
entity_id: number.${friendly_sensor_name}_progress_bottom | |
on_value: | |
then: | |
- lambda: |- | |
id(progress_bottom) = int(id(progress_bottom_sensor).state); | |
web_server: | |
sorting_group_id: sorting_group_display_settings | |
script: | |
- id: show_startup_message | |
mode: restart | |
then: | |
- lambda: |- | |
id(notification_message) = "Welcome! Device is starting up..."; | |
id(notification_active) = true; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- delay: 5s | |
- lambda: |- | |
id(display_update_triggered) = true; | |
id(startup_message_shown) = true; | |
- component.update: disp | |
- id: update_page | |
mode: restart | |
then: | |
- lambda: |- | |
id(display_update_triggered) = true; | |
- if: | |
condition: | |
lambda: "return id(page_rotation_switch).state;" | |
then: | |
- lambda: |- | |
id(page) = (id(page) % id(number_of_pages)) + 1; | |
else: | |
- lambda: |- | |
id(page) = id(fixed_page).state; | |
- delay: !lambda "return id(page_change_interval).state * 1000;" | |
- script.execute: update_page | |
- id: update_display | |
mode: single | |
then: | |
- lambda: |- | |
id(display_update_triggered) = true; | |
// Update the display content based on the current page sensor values | |
if (id(page) == 1) { | |
id(geekmagic_page_1).publish_state(id(geekmagic_page_1).state); | |
} else if (id(page) == 2) { | |
id(geekmagic_page_2).publish_state(id(geekmagic_page_2).state); | |
} else if (id(page) == 3) { | |
id(geekmagic_page_3).publish_state(id(geekmagic_page_3).state); | |
} else if (id(page) == 4) { | |
id(geekmagic_page_4).publish_state(id(geekmagic_page_4).state); | |
} else if (id(page) == 5) { | |
id(geekmagic_page_5).publish_state(id(geekmagic_page_5).state); | |
} | |
- component.update: disp | |
- delay: !lambda "return id(display_update_interval).state * 1000;" | |
- script.execute: update_display | |
- id: show_notification | |
mode: queued | |
then: | |
- lambda: |- | |
id(notification_active) = true; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- delay: !lambda "return id(notification_timeout) * 1000;" | |
- lambda: |- | |
id(notification_active) = false; | |
id(display_update_triggered) = true; | |
- component.update: disp | |
- id: display_update_loop | |
mode: restart | |
then: | |
- while: | |
condition: | |
lambda: "return true;" | |
then: | |
- if: | |
condition: | |
lambda: "return !id(display_update_triggered);" | |
then: | |
- component.update: disp | |
- lambda: |- | |
id(display_update_triggered) = false; | |
- delay: !lambda "return id(page_change_interval).state * 1000;" | |
- id: display_buzzer | |
mode: single | |
then: | |
- lambda: |- | |
id(current_brightness) = id(back_light).current_values.get_brightness(); // Store the brightness in a global variable | |
- repeat: | |
count: 4 | |
then: | |
- light.dim_relative: | |
id: back_light | |
relative_brightness: -0.99 | |
- delay: 500ms | |
- light.dim_relative: | |
id: back_light | |
relative_brightness: 0.99 | |
- delay: 500ms | |
- light.dim_relative: | |
id: back_light | |
relative_brightness: !lambda "return id(current_brightness) - id(back_light).current_values.get_brightness();" | |
- id: initialize_device | |
mode: restart | |
then: | |
- if: | |
condition: | |
lambda: "return id(first_boot);" | |
then: | |
- script.execute: show_startup_message | |
- lambda: |- | |
id(first_boot) = false; | |
- delay: 5s # Ensure the startup message is shown for 5 seconds before proceeding | |
- switch.turn_on: page_rotation_switch | |
- script.execute: update_page | |
- script.execute: update_display | |
- script.execute: display_update_loop | |
display: | |
- platform: st7789v | |
model: "Custom" | |
spi_id: spihwd | |
height: 240 | |
width: 240 | |
offset_height: 0 | |
offset_width: 0 | |
dc_pin: GPIO00 | |
reset_pin: GPIO02 | |
eightbitcolor: True | |
update_interval: 120s | |
id: disp | |
spi_mode: mode3 | |
lambda: |- | |
// Define average character widths and line heights | |
const int avg_char_width_normal = 9; | |
const int avg_char_width_small = 7; | |
const int avg_line_height_normal = 32; | |
const int avg_line_height_small = 26; | |
// Fill the global background color if it changes | |
if (id(background_color) != id(last_background_color)) { | |
it.fill(id(background_color)); // Apply global background | |
id(last_background_color) = id(background_color); | |
} | |
// Padding and progress bar adjustments | |
const int progress_bar_thickness = 3; | |
const int progress_bar_bottom_offset = 6; | |
const int y_start = id(tb_padding).state + (id(progress_top) > 0 ? progress_bar_thickness + 3 : 0); | |
int y = y_start; // Initial y position | |
int x = id(lr_padding).state; // Initial x position | |
// Column-related variables | |
const int display_width = it.get_width() - 2 * id(lr_padding).state; // Full display width | |
const int column_width = display_width / 2; // Width of each column | |
const int column_padding = 10; // Padding between columns | |
int left_x = id(lr_padding).state; // Start X for column 1 | |
int right_x = left_x + column_width + column_padding; // Start X for column 2 | |
int left_y = y_start; // Start Y for column 1 | |
int right_y = y_start; // Start Y for column 2 | |
bool in_column_1 = false; // Indicates if rendering in Column 1 | |
bool in_column_2 = false; // Indicates if rendering in Column 2 | |
// Default background colors for columns | |
Color column_1_bg_color = id(background_color); | |
Color column_2_bg_color = id(background_color); | |
const char* text = ""; // Placeholder for the text content | |
auto font = id(font_normal); | |
// Function to reset column positions | |
auto reset_column_positions = [&]() { | |
left_x = id(lr_padding).state; | |
right_x = left_x + column_width + column_padding; | |
left_y = y_start; // Reset Y for Column 1 | |
right_y = y_start; // Reset Y for Column 2 | |
}; | |
// Function to remove formatting markers from a string | |
auto remove_markers = [&](std::string &line) { | |
size_t marker_pos; | |
while ((marker_pos = line.find("[")) != std::string::npos) { | |
size_t end_marker_pos = line.find("]", marker_pos); | |
if (end_marker_pos != std::string::npos) { | |
line.erase(marker_pos, end_marker_pos - marker_pos + 1); | |
} else { | |
break; // Malformed marker | |
} | |
} | |
}; | |
// Function to render a single line with line wrapping | |
auto print_line = [&](int x, int &y, std::string line, Color color, TextAlign align, auto font, int avg_char_width, int avg_line_height) { | |
std::string clean_line = line; // Copy the line to clean formatting markers | |
remove_markers(clean_line); // Clean formatting markers for accurate line wrapping | |
// Determine the maximum characters per line based on the context (column or full width) | |
int usable_width = (in_column_1 || in_column_2) ? column_width - 2 * column_padding : display_width; | |
int max_chars_per_line = usable_width / avg_char_width; | |
// Wrap lines based on clean text length | |
while (clean_line.length() > max_chars_per_line) { | |
// Break the line at the max character limit | |
std::string sub_line = clean_line.substr(0, max_chars_per_line); | |
size_t last_space = sub_line.find_last_of(" "); // Try to break at a space | |
if (last_space != std::string::npos) { | |
sub_line = clean_line.substr(0, last_space); | |
clean_line.erase(0, last_space + 1); | |
} else { | |
sub_line += "-"; // Add a hyphen if no space is found | |
clean_line.erase(0, max_chars_per_line - 1); | |
} | |
// Render the wrapped line | |
it.printf(x, y, font, color, align, "%s", sub_line.c_str()); | |
y += avg_line_height; // Move to the next line | |
} | |
// Render the remaining part of the line | |
it.printf(x, y, font, color, align, "%s", clean_line.c_str()); | |
y += avg_line_height; // Adjust line height | |
}; | |
// Function to parse and render text with markers | |
auto parse_line = [&](std::string line) { | |
Color color = id(color_text); // Default text color | |
TextAlign align = TextAlign::LEFT; // Default text alignment | |
auto font = id(font_normal); // Default font | |
int avg_char_width = avg_char_width_normal; | |
int avg_line_height = avg_line_height_normal; | |
// Function to apply style markers (e.g., [red], [font_small]) | |
auto apply_style_markers = [&](std::string &line) { | |
auto find_and_replace = [&](std::string &line, const std::string &marker, auto &property, auto new_value) { | |
size_t pos = line.find(marker); | |
if (pos != std::string::npos) { | |
property = new_value; | |
line.erase(pos, marker.length()); | |
} | |
}; | |
// Apply text styles and font sizes | |
find_and_replace(line, "[font_small]", font, id(font_small)); | |
find_and_replace(line, "[font_large]", font, id(font_normal)); // Example for large font | |
find_and_replace(line, "[red]", color, id(color_red)); // Example for red text | |
// Alignment markers | |
find_and_replace(line, "[center]", align, TextAlign::CENTER); | |
find_and_replace(line, "[left]", align, TextAlign::LEFT); | |
find_and_replace(line, "[right]", align, TextAlign::RIGHT); | |
// Page initial marker | |
size_t initial_start_pos = line.find("[initial]"); | |
size_t initial_end_pos = line.find("[/initial]"); | |
if (initial_start_pos != std::string::npos && initial_end_pos != std::string::npos) { | |
id(page_initial) = line.substr(initial_start_pos + 9, initial_end_pos - initial_start_pos - 9).substr(0, 4); // Extract up to 4 characters | |
line.erase(initial_start_pos, initial_end_pos - initial_start_pos + 10); // Remove the marker and the initial | |
} | |
}; | |
// First, process column markers to set the rendering context | |
if (line.find("[column_1]") != std::string::npos) { | |
in_column_1 = true; | |
in_column_2 = false; | |
x = left_x; | |
y = left_y; | |
line.erase(line.find("[column_1]"), std::string("[column_1]").length()); | |
} else if (line.find("[/column_1]") != std::string::npos) { | |
in_column_1 = false; | |
left_y = y; // Update left_y after finishing Column 1 | |
line.erase(line.find("[/column_1]"), std::string("[/column_1]").length()); | |
} else if (line.find("[column_2]") != std::string::npos) { | |
in_column_2 = true; | |
in_column_1 = false; | |
x = right_x; | |
y = right_y; | |
line.erase(line.find("[column_2]"), std::string("[column_2]").length()); | |
} else if (line.find("[/column_2]") != std::string::npos) { | |
in_column_2 = false; | |
right_y = y; // Update right_y after finishing Column 2 | |
line.erase(line.find("[/column_2]"), std::string("[/column_2]").length()); | |
} | |
// Apply any remaining style markers | |
apply_style_markers(line); | |
// Render the line in the appropriate column or full display | |
if (in_column_1) { | |
print_line(left_x, left_y, line, color, align, font, avg_char_width, avg_line_height); | |
} else if (in_column_2) { | |
print_line(right_x, right_y, line, color, align, font, avg_char_width, avg_line_height); | |
} else { | |
print_line(x, y, line, color, align, font, avg_char_width, avg_line_height); // Full width | |
} | |
}; | |
// Reset the page initial at the start of rendering | |
id(page_initial) = ""; | |
// Determine the text to display based on the current page or notification | |
if (id(notification_active)) { | |
id(notification_active) = false; // Reset the notification flag | |
text = id(notification_message).c_str(); | |
} else { | |
switch (id(page)) { | |
case 1: | |
text = id(geekmagic_page_1).state.c_str(); | |
break; | |
case 2: | |
text = id(geekmagic_page_2).state.c_str(); | |
break; | |
case 3: | |
text = id(geekmagic_page_3).state.c_str(); | |
break; | |
case 4: | |
text = id(geekmagic_page_4).state.c_str(); | |
break; | |
case 5: | |
text = id(geekmagic_page_5).state.c_str(); | |
break; | |
default: | |
text = "Invalid page!"; | |
} | |
} | |
// Remove all line breaks and returns from the text | |
std::string text_str(text); | |
text_str.erase(std::remove(text_str.begin(), text_str.end(), '\n'), text_str.end()); | |
text_str.erase(std::remove(text_str.begin(), text_str.end(), '\r'), text_str.end()); | |
// Reset column positions at the start of rendering | |
reset_column_positions(); | |
size_t pos = 0; | |
while ((pos = text_str.find("\\n")) != std::string::npos) { | |
std::string line = text_str.substr(0, pos); | |
parse_line(line); | |
text_str.erase(0, pos + 2); | |
} | |
parse_line(text_str); // Render remaining text | |
// Draw progress bars | |
auto get_progress_color = [&](int progress) { | |
if (progress <= 25) { | |
return id(color_green); | |
} else if (progress <= 50) { | |
return id(color_yellow); | |
} else if (progress <= 75) { | |
return id(color_peach); | |
} else { | |
return id(color_red); | |
} | |
}; | |
int progress_top_width = (it.get_width() * id(progress_top)) / 100; | |
int progress_bottom_width = (it.get_width() * id(progress_bottom)) / 100; | |
// Top progress bar | |
if (id(progress_top) > 0) { | |
it.filled_rectangle(0, 0, progress_top_width, progress_bar_thickness, get_progress_color(id(progress_top))); | |
} | |
// Bottom progress bar | |
if (id(progress_bottom) > 0) { | |
it.filled_rectangle( | |
0, | |
it.get_height() - progress_bar_thickness - progress_bar_bottom_offset, | |
progress_bottom_width, | |
progress_bar_thickness, | |
get_progress_color(id(progress_bottom)) | |
); | |
} | |
// Function to render the page initial | |
auto render_page_initial = [&]() { | |
if (!id(page_initial).empty()) { | |
int initial_length = id(page_initial).length(); | |
int initial_x = it.get_width() - id(lr_padding).state - initial_length * 8; // Adjust x position based on initial length | |
int initial_y = it.get_height() - id(tb_padding).state - 28; // Small font height | |
it.printf(initial_x, initial_y, id(font_small), id(color_overlay0), TextAlign::RIGHT, "%s", id(page_initial).c_str()); | |
} | |
}; | |
// Render the page initial at the end of the display update | |
render_page_initial(); |
INFO ESPHome 2025.4.2
INFO Reading configuration /config/esphome/man-canh-bao.yaml...
INFO Detected timezone 'Asia/Saigon'
INFO Generating C++ source...
Traceback (most recent call last):
File "/usr/local/bin/esphome", line 8, in
sys.exit(main())
^^^^^^
File "/esphome/esphome/main.py", line 1081, in main
return run_esphome(sys.argv)
^^^^^^^^^^^^^^^^^^^^^
File "/esphome/esphome/main.py", line 1068, in run_esphome
rc = POST_CONFIG_ACTIONS[args.command](args, config)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/esphome/esphome/main.py", line 513, in command_run
exit_code = write_cpp(config)
^^^^^^^^^^^^^^^^^
File "/esphome/esphome/main.py", line 213, in write_cpp
generate_cpp_contents(config)
File "/esphome/esphome/main.py", line 225, in generate_cpp_contents
CORE.flush_tasks()
File "/esphome/esphome/core/init.py", line 673, in flush_tasks
self.event_loop.flush_tasks()
File "/esphome/esphome/coroutine.py", line 246, in flush_tasks
next(task.iterator)
File "/esphome/esphome/main.py", line 205, in wrapped
await coro(conf)
File "/data/external_components/b2fc4db2/esphome/components/st7789v/display.py", line 159, in to_code
await display.register_display(var, config)
File "/esphome/esphome/components/display/init.py", line 136, in register_display
await cg.register_component(var, config)
File "/esphome/esphome/cpp_helpers.py", line 53, in register_component
raise ValueError(
ValueError: Component ID disp was not declared to inherit from Component, or was registered twice. Please create a bug report with your configuration.
bị lổi vầy xử lý thế nào bạn ?
I’ve made some updates to my ESPHome configuration for the low-end GeekMagic TV:
• Removed unnecessary sensors and the logger to save RAM.
• Added an option to create two columns on a page.
• Enabled displaying a page initially in the bottom right corner.
• Included the API key in the API configuration for potential REST API functionality (not thoroughly tested yet).
• Updated the web server GUI to a newer, more user-friendly version.
• Added sorting to the web server UI.
• Settings are now saved between reboots.
• Fixed issues with display and sensor updates.
• Notifications are now queued for better handling.