Skip to content

Instantly share code, notes, and snippets.

@kmplngj
Last active April 14, 2025 10:36
Show Gist options
  • Save kmplngj/c02d0f3e0d68ad97dc4c2fcd3a0edb51 to your computer and use it in GitHub Desktop.
Save kmplngj/c02d0f3e0d68ad97dc4c2fcd3a0edb51 to your computer and use it in GitHub Desktop.
GeekMagic Display Small TV ESPHome Config
# 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();
@kmplngj
Copy link
Author

kmplngj commented Jan 2, 2025

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment