This commit is contained in:
2026-03-26 12:10:21 +01:00
parent 1f4970c17c
commit d4d76db890
877 changed files with 631941 additions and 26195 deletions

3
esphome/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"cmake.ignoreCMakeListsMissing": true
}

View File

@@ -0,0 +1,182 @@
substitutions:
device_name: "ws-esp32-s3-eth"
friendly_name: "meterkastinterface"
comment: "ESP32-s3 eth poe"
location: "meterkast"
# api_password: !secret esp_P1_api
# ota_password: !secret ota_password
# wifi_ssid: !secret wifi_ssid
# wifi_password: !secret wifi_password
# gateway: !secret ip_gateway
# subnet: !secret ip_subnet
# ip: !secret esp_P1_ip
board: "esp32-s3-devkitc-1"
framework: arduino #esp-idf
pin_data: GPIO21
pin_inp1: GPIO18
pin_out1: GPIO15
pin_out2: GPIO2
pin_eth_clk: GPIO13
pin_eth_mosi: GPIO11
pin_eth_miso: GPIO12
pin_eth_cs: GPIO14
pin_eth_irq: GPIO10
pin_eth_rst: GPIO9
packages:
eth: !include interfaces/eth_W5500.yaml
board: !include boards/esp32-gen.yaml
common: !include common/common.yaml
logger: !include templates/logger.yaml
esphome:
on_boot:
priority: 600
then:
# - switch.turn_on: zRST_gpio
# - delay: 15ms
# - switch.turn_off: zRST_gpio
#id(mdns0).add_extra_service({ "_zwave", "_tcp", 6639, {{"version", "1.0"}, {"name", "TubesZB"},{"serial_number", get_mac_address()}} });
external_components:
- source: github://oxan/esphome-stream-server
# Enable Home Assistant API
api:
reboot_timeout: 0s
ota:
platform: esphome
button:
- platform: restart
name: "Restart the ESP32 Device"
uart:
- id: uart_bus_zw
rx_pin: GPIO17
tx_pin: GPIO16
baud_rate: 115200
- id: uart_dsmr
rx_pin: ${pin_data}
baud_rate: 115200
data_bits: 8
parity: NONE
stop_bits: 1
stream_server:
- id: zw
uart_id: uart_bus_zw
port: 6639
# - id: dsmr
# uart_id: uart_dsmr
# port: 23
switch:
- platform: gpio
pin: ${pin_out1}
name: "output 1"
- platform: gpio
pin: ${pin_out2}
name: "output 2"
binary_sensor:
- platform: stream_server
stream_server: zw
connected:
name: "TubesZB Z-Wave Serial Connected"
# - platform: stream_server
# stream_server: dsmr
# connected:
# name: "TubesZB DSMR Serial Connected"
- platform: gpio
pin: ${pin_inp1}
name: "input"
dsmr:
max_telegram_length: 1700
uart_id: uart_dsmr
sensor:
- platform: dsmr
energy_delivered_tariff1:
name: "Energy Consumed Tariff 1"
energy_delivered_tariff2:
name: "Energy Consumed Tariff 2"
energy_returned_tariff1:
name: "Energy Produced Tariff 1"
energy_returned_tariff2:
name: "Energy Produced Tariff 2"
power_delivered:
name: "Power Consumed"
accuracy_decimals: 3
power_returned:
name: "Power Produced"
accuracy_decimals: 3
electricity_failures:
name: "Electricity Failures"
icon: mdi:alert
electricity_long_failures:
name: "Long Electricity Failures"
icon: mdi:alert
voltage_l1:
name: "Voltage Phase 1"
voltage_l2:
name: "Voltage Phase 2"
voltage_l3:
name: "Voltage Phase 3"
current_l1:
name: "Current Phase 1"
current_l2:
name: "Current Phase 2"
current_l3:
name: "Current Phase 3"
power_delivered_l1:
name: "Power Consumed Phase 1"
accuracy_decimals: 3
power_delivered_l2:
name: "Power Consumed Phase 2"
accuracy_decimals: 3
power_delivered_l3:
name: "Power Consumed Phase 3"
accuracy_decimals: 3
power_returned_l1:
name: "Power Produced Phase 1"
accuracy_decimals: 3
power_returned_l2:
name: "Power Produced Phase 2"
accuracy_decimals: 3
power_returned_l3:
name: "Power Produced Phase 3"
accuracy_decimals: 3
gas_delivered:
name: "Gas Consumed"
- platform: uptime
name: "SlimmeLezer Uptime"
# - platform: wifi_signal
# name: "SlimmeLezer Wi-Fi Signal"
# update_interval: 60s
text_sensor:
- platform: dsmr
identification:
name: "DSMR Identification"
p1_version:
name: "DSMR Version"
# - platform: wifi_info
# ip_address:
# name: "SlimmeLezer IP Address"
# ssid:
# name: "SlimmeLezer Wi-Fi SSID"
# bssid:
# name: "SlimmeLezer Wi-Fi BSSID"
# - platform: version
# name: "ESPHome Version"
# hide_timestamp: false
# mdns:
# id: mdns0

37
esphome/aqs_ikea_co2.yaml Normal file
View File

@@ -0,0 +1,37 @@
substitutions:
device_name: "esp32-c3-aqs-ikea-co2"
friendly_name: "AQS-ikea-co2"
comment: "esp32, pm, co2, display, BTproxy"
location: "zolder"
board: "esp32-c3-devkitm-1"
framework: esp-idf
api_password: !secret air_quality_zolder_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
wifi_ssid2: !secret wifi_ssid2
wifi_password2: !secret wifi_password2
#gateway: !secret ip_gateway
#subnet: !secret ip_subnet
#ip: !secret aqs_ikea_co2_ip
update_interval: 1s
pin_status: GPIO8
pin_sda: GPIO6
pin_scl: GPIO7
packages:
board: !include boards/esp32-gen.yaml
i2c: !include interfaces/i2c_a.yaml
device_base: !include common/common.yaml
connection: !include common/wifi.yaml
status: !include templates/status.yaml
logger: !include templates/logger.yaml
#bt_proxy: !include common/bluetooth.yaml
#leds: !include templates/light_rgbw_rmt.yaml
#sensors
co2: !include sensors/scd30.yaml
tvoc: !include sensors/sgp30.yaml

View File

@@ -1,16 +1,19 @@
substitutions:
device_name: "aqs-woonkamer2"
friendly_name: "AQS-woonkamer2"
device_name: "aqs-slaapkamer"
friendly_name: "AQS-Slaapkamer"
comment: "esp32, pm, co2, temp, hum, occup, btprox"
location: "woonkamer"
api_password: !secret air_quality_woonkamer_api
location: "Slaapkamer Willem"
api_password: !secret air_quality_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
gateway: !secret ip_gateway
subnet: !secret ip_subnet
ip: !secret air_quality_woonkamer_ip
#ip: !secret air_quality_ip
update_interval: 30s
#hardware
board: "esp32dev"
framework: esp-idf
pin_status: GPIO2
pin_sda: GPIO21
pin_scl: GPIO22
@@ -18,13 +21,18 @@ substitutions:
pin_pm_tx: GPIO19
pin_ld_tx: GPIO16
pin_ld_rx: GPIO17
pin_leds: GPIO23
pin_led1: GPIO23
num_leds: "3"
chipset: ws2812
is_rgbw: "false"
rgb_order: GRB
packages:
board: !include boards/esp32_wroom_arduino.yaml
i2c: !include interfaces/i2c_a.yaml
board: !include boards/esp32-gen.yaml
i2c: !include interfaces/i2c_a.yaml
device_base: !include common/common.yaml
connection: !include common/wifi.yaml
connection: !include common/wifi.yaml
status: !include templates/status.yaml
logger: !include templates/nologger.yaml
bt_proxy: !include common/bluetooth.yaml
@@ -33,32 +41,16 @@ packages:
pmsc: !include sensors/pmsx0003.yaml
co2: !include sensors/scd30.yaml
tvoc: !include sensors/sgp30.yaml
led: !include templates/light_rgbw_rmt_nofx.yaml
light:
- platform: neopixelbus
type: GRBW
variant: ws2812X
pin: ${pin_leds}
num_leds: 3
name: "${device_name}_RGB_Light"
id: RGB_Light
effects:
- random:
name: "Random"
transition_length: 4s
update_interval: 5s
- addressable_rainbow:
name: Rainbow Effect
speed: 100
width: 2
- platform: partition
name: "Top_LED"
segments:
# Use first 10 LEDs from the light with ID light1
- id: RGB_Light
- id: led_lights
from: 2
to: 2
@@ -66,7 +58,7 @@ light:
name: "Middle_LED"
segments:
# Use first 10 LEDs from the light with ID light1
- id: RGB_Light
- id: led_lights
from: 1
to: 1
@@ -74,7 +66,7 @@ light:
name: "Bottom_LED"
segments:
# Use first 10 LEDs from the light with ID light1
- id: RGB_Light
- id: led_lights
from: 0
to: 0

View File

@@ -0,0 +1,157 @@
---
substitutions:
device_name: slimmelezer
friendly_name: "slimmelezer"
comment: "esp8266"
api_password: !secret slimmelezer_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
wifi_ssid2: !secret wifi_ssid2
wifi_password2: !secret wifi_password2
location: "meterkast"
packages:
device_base: !include common/common.yaml
connection: !include common/wifi.yaml
esp8266:
restore_from_flash: true
board: d1_mini
wifi:
# Powersaving for brownout due to 250mA restriction P1
output_power: 14dB
#captive_portal:
# Enable logging
logger:
baud_rate: 0
# logs:
# component: ERROR
# Enable Home Assistant API
# api:
# ota:
# platform: esphome
# external_components:
# - source: github://oxan/esphome-stream-server
# stream_server:
# - id: dsmrreaderserver
# uart_id: dsmrreaderuart
# port: 6640
# binary_sensor:
# - platform: stream_server
# stream_server: dsmrreaderserver
# connected:
# name: "DSMR Serial Connected"
uart:
- id: dsmrreaderuart
baud_rate: 9600
rx_pin: D7
rx_buffer_size: 1700
parity: EVEN
data_bits: 7
stop_bits: 1
# globals:
# - id: has_key
# type: bool
# restore_value: yes
# initial_value: "false"
# - id: stored_decryption_key
# type: char[32]
# restore_value: yes
dsmr:
id: dsmr_instance
max_telegram_length: 1700
crc_check: false
sensor:
- platform: dsmr
energy_delivered_tariff1:
name: "Energy Consumed Tariff 1"
energy_delivered_tariff2:
name: "Energy Consumed Tariff 2"
energy_returned_tariff1:
name: "Energy Produced Tariff 1"
energy_returned_tariff2:
name: "Energy Produced Tariff 2"
power_delivered:
name: "Power Consumed"
accuracy_decimals: 3
power_returned:
name: "Power Produced"
accuracy_decimals: 3
electricity_failures:
name: "Electricity Failures"
icon: mdi:alert
electricity_long_failures:
name: "Long Electricity Failures"
icon: mdi:alert
voltage_l1:
name: "Voltage Phase 1"
voltage_l2:
name: "Voltage Phase 2"
voltage_l3:
name: "Voltage Phase 3"
current_l1:
name: "Current Phase 1"
current_l2:
name: "Current Phase 2"
current_l3:
name: "Current Phase 3"
power_delivered_l1:
name: "Power Consumed Phase 1"
accuracy_decimals: 3
power_delivered_l2:
name: "Power Consumed Phase 2"
accuracy_decimals: 3
power_delivered_l3:
name: "Power Consumed Phase 3"
accuracy_decimals: 3
power_returned_l1:
name: "Power Produced Phase 1"
accuracy_decimals: 3
power_returned_l2:
name: "Power Produced Phase 2"
accuracy_decimals: 3
power_returned_l3:
name: "Power Produced Phase 3"
accuracy_decimals: 3
gas_delivered:
name: "Gas Consumed"
- platform: uptime
name: "SlimmeLezer Uptime"
- platform: wifi_signal
name: "SlimmeLezer Wi-Fi Signal"
update_interval: 60s
text_sensor:
- platform: dsmr
identification:
name: "DSMR Identification"
p1_version:
name: "DSMR Version"
# p1_version_be:
# name: "DSMR Version Belgium"
# timestamp:
# name: "Timestamp"
# - platform: wifi_info
# ip_address:
# name: "SlimmeLezer IP Address"
# ssid:
# name: "SlimmeLezer Wi-Fi SSID"
# bssid:
# name: "SlimmeLezer Wi-Fi BSSID"
# - platform: version
# name: "ESPHome Version"
# hide_timestamp: true

View File

@@ -0,0 +1,58 @@
esphome:
name: esp32-c6-2
friendly_name: esp32-c6-2
api:
encryption:
key: !secret ot_ftd_led
ota:
- platform: esphome
password: !secret ota_password
esp32:
board: esp32-c6-devkitm-1
framework:
type: esp-idf
logger:
network:
enable_ipv6: true
openthread:
device_type: FTD
tlv: !secret otbr_tlv
output:
- platform: gpio
pin: GPIO15
id: light_output
light:
- platform: binary
name: "Desk Lamp"
output: light_output
text_sensor:
- platform: openthread_info
ip_address:
name: "IP Address"
channel:
name: "Channel"
role:
name: "Device Role"
rloc16:
name: "RLOC16"
ext_addr:
name: "Extended Address"
eui64:
name: "EUI64 Interface ID"
network_name:
name: "Network Name"
network_key:
name: "Network Key"
pan_id:
name: "PAN ID"
ext_pan_id:
name: "Extended PAN ID"

View File

@@ -0,0 +1,32 @@
esphome:
name: esp32-nixie
friendly_name: ESP32-nixie
esp32:
board: esp32dev
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "4iPpRwvWoiq/euJoxnwpPiw+VfJcDxx/SPBtTT0VWRw="
ota:
- platform: esphome
password: "bc3f2661a30d3f60973c4158fcd73bd3"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Esp32-Nixie Fallback Hotspot"
password: "Knp1shnjPoDY"
captive_portal:

32
esphome/archive/eth.yaml Normal file
View File

@@ -0,0 +1,32 @@
esphome:
name: eth
friendly_name: eth
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "tHL0kVpQa5LCE0MLdxEVjLTVGZahEH0pIiO3VklxnKM="
ota:
- platform: esphome
password: "2eb3e4396a094ae7ba24cf54cda85e26"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Eth Fallback Hotspot"
password: "foGYMGevQflx"
captive_portal:

View File

@@ -0,0 +1,25 @@
substitutions:
device_name: "slaaptrainer"
friendly_name: "slaaptrainer Tim"
comment: "esp8266, RGBled"
location: "slaapkamer tim"
api_password: !secret slaaptrainer_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
wifi_ssid2: !secret wifi_ssid2
wifi_password2: !secret wifi_password2
gateway: !secret ip_gateway
subnet: !secret ip_subnet
ip: !secret slaaptrainer_ip
pin_led1: GPIO03
num_leds: "35"
packages:
board: !include boards/esp12f.yaml
connection: !include common/wifi.yaml
device_base: !include common/common.yaml
logger: !include templates/nologger.yaml
leds: !include templates/light_neopixel.yaml
colororange: !include color/COLOR_CSS_ORANGE
colorgreen: !include color/COLOR_CSS_GREEN

View File

@@ -0,0 +1,32 @@
esphome:
name: thread-repeater
friendly_name: thread-repeater
esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "dDrJ5CD+x5R0yTru0MlfY9y9JaW8+4x6WWvha6Jvyuw="
ota:
- platform: esphome
password: "c5b6017a482b9e783b6e88edfc53992b"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Thread-Repeater Fallback Hotspot"
password: "LkDplhFydNZd"
captive_portal:

BIN
esphome/beep.wav Normal file

Binary file not shown.

Submodule esphome/boards/.esphome/external_components/2633138a added at d59be2c3a5

Submodule esphome/boards/.esphome/external_components/3a17a31a updated: 5f1a08c922...e8f3eefd49

View File

@@ -0,0 +1,8 @@
esp32:
board: esp32-p4-evboard
cpu_frequency: 400MHz
flash_size: 16MB
framework:
type: esp-idf
advanced:
enable_idf_experimental_features: yes

View File

@@ -2,4 +2,5 @@
esp32:
board: ${board}
framework:
type: ${framework}
type: ${framework}
#version: 5.4.1

View File

@@ -0,0 +1,16 @@
---
esp32:
board: ${board}
#variant: ${variant}
flash_size: ${flashsize}
framework:
type: ${framework}
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: y
# CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
# CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
# CONFIG_SPIRAM_RODATA: y
# CONFIG_FREERTOS_USE_TRACE_FACILITY: "y"
# CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS: "y"

View File

@@ -0,0 +1,3 @@
psram:
mode: octal
speed: 80MHz

View File

@@ -1,23 +1,20 @@
substitutions:
device_name: "btproxykeuken"
friendly_name: "BT_proxy_keuken"
device_name: "btproxywoonkamer"
friendly_name: "BT_proxy_woonkamer"
comment: "ESP32-c3 BTproxy"
location: "keuken"
location: "woonkamer"
board: "esp32-c3-devkitm-1"
framework: esp-idf
api_password: !secret bt_proxy_keuken_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
gateway: !secret ip_gateway
subnet: !secret ip_subnet
ip: !secret bt_proxy_keuken_ip
pin_status: GPIO8
packages:
board: !include boards/esp32-gen.yaml
connection: !include common/wifi_btprox.yaml
connection: !include common/wifi.yaml
device_base: !include common/common.yaml
logger: !include templates/logger.yaml
btproxy: !include templates/bt_proxy.yaml

View File

@@ -0,0 +1,107 @@
substitutions:
device_name: "display4848"
friendly_name: "display lgvl"
comment: "esp32-s3"
location: "Woonkamer"
board: "esp32-s3-devkitc-1"
variant: "esp32s3"
flashsize: "16MB"
framework: "esp-idf"
api_password: !secret display_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
wifi_ssid2: !secret wifi_ssid2
wifi_password2: !secret wifi_password2
packages:
board: !include boards/esp32-variant.yaml
psram: !include boards/psram.yaml
common: !include common/common.yaml
connected: !include common/wifi_nosens.yaml
display: !include display/guition480480.yaml
logger: !include templates/logger.yaml
#widgets and pages
home: !include widgets/home/home.yaml
lights_config: !include widgets/light/lights_config.yaml
devices: !include widgets/devices.yaml
settings: !include widgets/settings.yaml
menu_controls_main: !include widgets/menu_controls_main.yaml
loading_page: !include widgets/loading_page.yaml
image: !include widgets/image.yaml
font: !include widgets/fonts.yaml
color: !include widgets/colors.yaml
http_request:
verify_ssl: false
external_components:
- source: github://pr#9972
components: [mapping]
refresh: 1h
esphome:
# name: display
# friendly_name: display
includes:
- <sstream>
- <algorithm>
platformio_options:
board_build.flash_mode: dio
lvgl:
color_depth: 16
byte_order: big_endian
displays: my_display
touchscreens:
- touchscreen_id: my_touchscreen
long_press_time: 5000ms
long_press_repeat_time: 400ms
page_wrap: false
# interval:
# - interval: 60s
# then:
# - lambda: |-
# // Общая информация о памяти
# ESP_LOGI("memory", "Free heap: %d bytes", esp_get_free_heap_size());
# ESP_LOGI("memory", "Free internal heap: %d bytes", esp_get_free_internal_heap_size());
# ESP_LOGI("memory", "Min free heap: %d bytes", esp_get_minimum_free_heap_size());
# // Детальная статистика кучи
# multi_heap_info_t info;
# heap_caps_get_info(&info, MALLOC_CAP_INTERNAL);
# ESP_LOGI("memory", "SRAM total: %d, free: %d, largest_free: %d",
# info.total_free_bytes + info.total_allocated_bytes,
# info.total_free_bytes,
# info.largest_free_block);
# // Статистика по PSRAM
# heap_caps_get_info(&info, MALLOC_CAP_SPIRAM);
# ESP_LOGI("memory", "PSRAM total: %d, free: %d, largest_free: %d",
# info.total_free_bytes + info.total_allocated_bytes,
# info.total_free_bytes,
# info.largest_free_block);
# - lambda: |-
# ESP_LOGI("stack", "Main task free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("loopTask")));
# ESP_LOGI("stack", "System event free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("sys_evt")));
# ESP_LOGI("stack", "Timer task free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("esp_timer")));
# - lambda: |-
# char* buf = (char*)malloc(1024);
# if (buf) {
# vTaskList(buf);
# ESP_LOGI("stack", "\nTask Name\tState\tPrio\tStack\tNum\n%s", buf);
# free(buf);
# }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,117 @@
substitutions:
board: esp32-s3-devkitc-1
framework: arduino
device_name: "deurbel"
friendly_name: Deurbel
comment: "ESP32-cam-button"
ssid: !secret wifi_ssid
password: !secret wifi_password
api_password: !secret doorcam_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
# Pin define
#led sk6812-EX20
pin_led1: GPIO33
# Camera esp32-spk
pin_cam_d0: GPIO2 # Camera Data pin 0 - cam_Y2
pin_cam_d1: GPIO3 # Camera Data pin 1 - cam_Y3
pin_cam_d2: GPIO4 # Camera Data pin 2 - cam_Y4
pin_cam_d3: GPIO5 # Camera Data pin 3 - cam_Y5
pin_cam_d4: GPIO6 # Camera Data pin 4 - cam_Y6
pin_cam_d5: GPIO41 # Camera Data pin 5 - cam_Y7
pin_cam_d6: GPIO48 # Camera Data pin 6 - cam_Y8
pin_cam_d7: GPIO47 # Camera Data pin 7 - cam_Y9
pin_cam_vsync: GPIO35 # Camera VSYNC
pin_cam_href: GPIO34 # pin_pin_camera HREF
pin_cam_pclk: GPIO41 # pin_camera Pixel Clock
pin_cam_xclk: GPIO33 # pin_camera External Clock
pin_cam_sda: GPIO37 # Camera SDA
pin_cam_scl: GPIO36 # pin_camera SCK
#audio
pin_mic_data: GPIO38
pin_mic_sck: GPIO39
pin_mic_ws: GPIO40
pin_amp_ctrl: GPIO46
pin_amp_ws: GPIO45
pin_amp_bclk: GPIO19
pin_amp_data: GPIO9
#sdcard - SPI
pin_D0: GPIO12 #MISO
pin_D3: GPIO2 #CS
pin_CMD: GPIO3 #mosi
pin_SCLK: GPIO11 #CLK
esphome:
name: ${device_name}
friendly_name: ${friendly_name}
#min_version: 2025.9.0
#name_add_mac_suffix: false
esp32:
board: ${board}
variant: esp32s3
framework:
type: ${framework}
# Enable logging
logger:
# Enable Home Assistant API
api:
# Allow Over-The-Air updates
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# SPI bus configuratie
# spi:
# clk_pin: GPIO12
# miso_pin: GPIO13
# mosi_pin: GPIO11
# # RC522 configuratie
# rc522_spi:
# cs_pin: GPIO10
# reset_pin: GPIO9
# on_tag:
# then:
# - homeassistant.tag_scanned: !lambda 'return x;'
# - logger.log:
# format: "Tag gescand: %s"
# args: [ 'x.c_str()' ]
psram:
i2s_audio:
id: i2s_in
i2s_lrclk_pin: ${pin_mic_ws}
i2s_bclk_pin: ${pin_amp_bclk}
media_player:
- platform: i2s_audio
name: "esp_speaker"
id: media_player_speaker
i2s_audio_id: i2s_in
dac_type: external
i2s_dout_pin: ${pin_amp_data}
mode: mono
on_play:
- logger.log: "Media playing!"
- media_player.volume_set:
id: media_player_speaker
volume: 100%
microphone:
- platform: i2s_audio
id: external_mic
adc_type: external
i2s_din_pin: ${pin_mic_data}

View File

@@ -0,0 +1,102 @@
substitutions:
board: esp32-s3-devkitc-1
framework: esp-idf
device_name: esphome-web-0c8784
friendly_name: deurbell 2.0
comment: "ESP32-cam-button"
ssid: !secret wifi_ssid
password: !secret wifi_password
api_password: !secret doorcam_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
# Pin define
#led sk6812-EX20
pin_led1: GPIO21
# Camera esp32-spk
pin_cam_d0: GPIO7 # Camera Data pin 0 - cam_Y2
pin_cam_d1: GPIO5 # Camera Data pin 1 - cam_Y3
pin_cam_d2: GPIO4 # Camera Data pin 2 - cam_Y4
pin_cam_d3: GPIO6 # Camera Data pin 3 - cam_Y5
pin_cam_d4: GPIO8 # Camera Data pin 4 - cam_Y6
pin_cam_d5: GPIO42 # Camera Data pin 5 - cam_Y7
pin_cam_d6: GPIO48 # Camera Data pin 6 - cam_Y8
pin_cam_d7: GPIO47 # Camera Data pin 7 - cam_Y9
pin_cam_vsync: GPIO35 # Camera VSYNC
pin_cam_href: GPIO34 # pin_pin_camera HREF
pin_cam_pclk: GPIO41 # pin_camera Pixel Clock
pin_cam_xclk: GPIO33 # pin_camera External Clock
pin_cam_sda: GPIO37 # Camera SDA
pin_cam_scl: GPIO36 # pin_camera SCK
#audio
pin_mic_data: GPIO38
pin_mic_sck: GPIO39
pin_mic_ws: GPIO40
pin_amp_ctrl: GPIO46
pin_amp_lrclk: GPIO45
pin_amp_bclk: GPIO10
pin_amp_data: GPIO9
#sdcard - SPI
pin_D0: GPIO12 #MISO
pin_D3: GPIO2 #CS
pin_CMD: GPIO3 #mosi
pin_SCLK: GPIO11 #CLK
#buttons:
pin_sw1: GPIO15
pin_sw2: GPIO16
packages:
board: !include boards/esp32-gen.yaml
common: !include common/common.yaml
wifi: !include common/wifi.yaml
logger: !include templates/logger.yaml
time: !include templates/time.yaml
audio: !include interfaces/audio.yaml
psram:
esp32_camera:
name: camera
external_clock:
pin: $pin_cam_xclk
frequency: 20MHz
i2c_pins:
sda: $pin_cam_sda
scl: $pin_cam_scl
data_pins: [$pin_cam_d0, $pin_cam_d1, $pin_cam_d2, $pin_cam_d3, $pin_cam_d4, $pin_cam_d5, $pin_cam_d6, $pin_cam_d7]
vsync_pin: $pin_cam_vsync
href_pin: $pin_cam_href
pixel_clock_pin: $pin_cam_pclk
vertical_flip: false
horizontal_mirror: false
# resolution: 320x240
binary_sensor:
- platform: gpio
pin: ${pin_sw1}
name: "sw1"
- platform: gpio
pin: ${pin_sw2}
name: "sw2"
#SPI bus configuratie
spi:
clk_pin: GPIO12
miso_pin: GPIO13
mosi_pin: GPIO11
# RC522 configuratie
rc522_spi:
cs_pin: GPIO14
reset_pin: GPIO1
on_tag:
then:
- homeassistant.tag_scanned: !lambda 'return x;'
- logger.log:
format: "Tag gescand: %s"
args: [ 'x.c_str()' ]

View File

@@ -429,12 +429,12 @@ display:
// afval
int waste_yoffset = drvtime_yoffset+90;
int waste_xoffset = 60;
it.printf(waste_xoffset, waste_yoffset, id(font_mdi_medium), COLOR_RED, TextAlign::TOP_CENTER, "%s", weather_icon_map["trash"].c_str());
// it.printf(waste_xoffset, waste_yoffset, id(font_mdi_medium), COLOR_RED, TextAlign::TOP_CENTER, "%s", weather_icon_map["trash"].c_str());
it.printf(waste_xoffset+100, waste_yoffset, id(font_small_bold), TextAlign::TOP_CENTER, "Vandaag:");
it.printf(waste_xoffset+260, waste_yoffset, id(font_small_bold), TextAlign::TOP_CENTER, "Morgen:");
it.printf(waste_xoffset+95, waste_yoffset+30, id(font_small_bold), TextAlign::TOP_CENTER, "%s", id(afval_today).state.c_str());
it.printf(waste_xoffset+255, waste_yoffset+30, id(font_small_bold), TextAlign::TOP_CENTER, "%s", id(afval_tomorrow).state.c_str());
// it.printf(waste_xoffset+100, waste_yoffset, id(font_small_bold), TextAlign::TOP_CENTER, "Vandaag:");
// it.printf(waste_xoffset+260, waste_yoffset, id(font_small_bold), TextAlign::TOP_CENTER, "Morgen:");
// it.printf(waste_xoffset+95, waste_yoffset+30, id(font_small_bold), TextAlign::TOP_CENTER, "%s", id(afval_today).state.c_str());
// it.printf(waste_xoffset+255, waste_yoffset+30, id(font_small_bold), TextAlign::TOP_CENTER, "%s", id(afval_tomorrow).state.c_str());
// it.line(30, 400, 440, 400);

View File

@@ -0,0 +1,232 @@
substitutions:
board: esp32-s3-devkitc-1
framework: esp-idf
device_name: doorcam
friendly_name: doorcam
comment: "ESP32-cam-button"
ssid: !secret wifi_ssid
password: !secret wifi_password
api_password: !secret doorcam_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
# Pin define
# SPI
# pin_spi_clk: GPIO21 # Serial Clock
# pin_spi_mosi: GPIO19 # Main Out Sub In
# pin_spi_miso: GPIO22 # Main In Sub Out
# tft
# pin_dis_cs: GPIO12 # Chip Select
# pin_dis_dc: GPIO15 # Data/Command
# pin_dis_bk: GPIO2 # Backlight
# Camera ttgo
# pin_cam_d0: GPIO34 # Camera Data pin 0
# pin_cam_d1: GPIO13 # Camera Data pin 1
# pin_cam_d2: GPIO26 # Camera Data pin 2
# pin_cam_d3: GPIO35 # Camera Data pin 3
# pin_cam_d4: GPIO39 # Camera Data pin 4
# pin_cam_d5: GPIO38 # Camera Data pin 5
# pin_cam_d6: GPIO37 # Camera Data pin 6
# pin_cam_d7: GPIO36 # Camera Data pin 7
# pin_cam_vsync: GPIO5 # Camera VSYNC
# pin_cam_href: GPIO27 # pin_pin_camera HREF
# pin_cam_pclk: GPIO25 # pin_camera Pixel Clock
# pin_cam_xclk: GPIO4 # pin_camera External Clock
# pin_cam_sda: GPIO18 # Camera SDA
# pin_cam_scl: GPIO23 # pin_camera SCL
#led sk6812-EX20
pin_led1: GPIO33
# Camera esp32-spk
pin_cam_d0: GPIO2 # Camera Data pin 0 - cam_Y2
pin_cam_d1: GPIO3 # Camera Data pin 1 - cam_Y3
pin_cam_d2: GPIO4 # Camera Data pin 2 - cam_Y4
pin_cam_d3: GPIO5 # Camera Data pin 3 - cam_Y5
pin_cam_d4: GPIO6 # Camera Data pin 4 - cam_Y6
pin_cam_d5: GPIO41 # Camera Data pin 5 - cam_Y7
pin_cam_d6: GPIO48 # Camera Data pin 6 - cam_Y8
pin_cam_d7: GPIO47 # Camera Data pin 7 - cam_Y9
pin_cam_vsync: GPIO35 # Camera VSYNC
pin_cam_href: GPIO34 # pin_pin_camera HREF
pin_cam_pclk: GPIO41 # pin_camera Pixel Clock
pin_cam_xclk: GPIO33 # pin_camera External Clock
pin_cam_sda: GPIO37 # Camera SDA
pin_cam_scl: GPIO36 # pin_camera SCK
#audio
pin_mic_data: GPIO38
pin_mic_sck: GPIO39
pin_mic_ws: GPIO40
pin_amp_ctrl: GPIO46
pin_amp_ws: GPIO45
pin_amp_bclk: GPIO19
pin_amp_data: GPIO9
#sdcard - SPI
pin_D0: GPIO12 #MISO
pin_D3: GPIO2 #CS
pin_CMD: GPIO3 #mosi
pin_SCLK: GPIO11 #CLK
# SD Card (TF) CS (CD/DAT3) GPIO2 SPI mode (SD_CS)
# DI (CMD/MOSI) GPIO3 SPI MOSI
# SCLK (CLK) GPIO11 SPI SCK
# DO (DATA0/MISO) GPIO12 SPI MISO
# LED SK6812-EX20 DIN GPIO21 Addressable RGB LED
# Camera (24-pin)
# Y2 GPIO2 - D0
# Y3 GPIO3 - D1
# Y4 GPIO4 Camera data - D2
# Y5 GPIO5 Camera data - D3
# Y6 GPIO6 Camera data - D4
# Y7 GPIO42 Camera data - D5
# Y8 GPIO48 Camera data - D6
# MCLK GPIO33 Master clock
# Y9 GPIO47 Camera data - D7
# HS (HREF/SYNC) GPIO34 Horizontal sync
# VS (VSYNC) GPIO35 Vertical sync
# SCK (SCCB/I2C) GPIO36 Camera config clock
# SDA (SCCB/I2C) GPIO37 Camera config data
# Microphones MSM261S SO (data) GPIO38 Both mics, shared line
# (MEMS, digital) MSM261S SCK (clk) GPIO39 Clock input
# MSM261S WS (sync) GPIO40 Word select
# Speaker Amp NS4168 CTRL GPIO46 Amplifier control
# Audio Out (I2S) LRCLK GPIO45 I2S word select
# BCLK GPIO19 I2S bit clock
# SDATA GPIO9 I2S serial data
psram:
mode: quad
speed: 80MHz
packages:
board: !include boards/esp32-gen.yaml
common: !include common/common.yaml
wifi: !include common/wifi.yaml
logger: !include templates/logger.yaml
# lcd: !include display/st7789v_t-cameraplus.yaml
time: !include templates/time.yaml
web_server:
color: !include widgets/colors.yaml
# I2C Bus Addresses
# 0x30 Camera OV2640 2Megapixel
# 0x68 MPU6050 Accelerometer/Gyroscope Sensor
# 0x75 IP5306 Battery Management
i2c:
- id: bus_a
sda: $pin_cam_sda
scl: $pin_cam_scl
# TTGO Camera Plus
# OV2640 2Megapixel
esp32_camera:
name: camera
external_clock:
pin: $pin_cam_xclk
frequency: 20MHz
i2c_pins:
sda: $pin_cam_sda
scl: $pin_cam_scl
data_pins: [$pin_cam_d0, $pin_cam_d1, $pin_cam_d2, $pin_cam_d3, $pin_cam_d4, $pin_cam_d5, $pin_cam_d6, $pin_cam_d7]
vsync_pin: $pin_cam_vsync
href_pin: $pin_cam_href
pixel_clock_pin: $pin_cam_pclk
vertical_flip: false
horizontal_mirror: false
# resolution: 320x240
# internal: true
# Camera Web Server
esp32_camera_web_server:
- port: 8080
mode: stream
- port: 8081
mode: snapshot
# lvgl:
# buffer_size: 100%
# byte_order: little_endian
# displays: my_display
# widgets:
# - button:
# id: button1_btn
# x: 20
# y: 20
# width: 100
# height: 100
# align: TOP_LEFT
# bg_color: color_steel_blue
# bg_opa: 20%
# shadow_opa: TRANSP
# radius: 10
# widgets:
# - label:
# id: light_on
# align: CENTER
# text_color: color_steel_blue
# text_font: icons_90
# text: "\U0000e908" # lightbulb
# - label:
# id: lable_name
# align: TOP_MID
# text_font: nunito_16
# text_color: color_misty_blue
# text: "hi"
# font:
# - file: "fonts/Nunito-SemiBold.ttf"
# id: nunito_16
# size: 16
# bpp: 4
# glyphsets:
# - GF_Latin_Core
# # - GF_Greek_Core
# # - GF_Cyrillic_Core
# # - GF_Latin_Vietnamese
# # glyphs: "²"
# # extras:
# # - file: "fonts/Jua-Regular.ttf"
# # glyphs: [
# # "\U0000C774",
# # "\U0000B8E8",
# # "\U0000B9C8",
# # ]
# - file: "fonts/icons_v2.ttf"
# id: icons_90
# size: 90
# bpp: 4
# glyphs: [
# "\U0000e908", # lightbulb
# # "\U0000e915", # spotlights_group
# # "\U0000e916", # desk_lamp
# # "\U0000e917", # pendant_lamp
# # "\U0000e918", # ceiling_lamp
# # "\U0000e921", # ceiling_lamp_variant
# # "\U0000e919", # night_lamp
# # "\U0000e91d", # 0 - shutter_closed
# # "\U0000e93d", # 10 - shutter
# # "\U0000e93e", # 20 - shutter
# # "\U0000e93f", # 30 - shutter
# # "\U0000e940", # 40 - shutter
# # "\U0000e941", # 50 - shutter
# # "\U0000e93c", # 60 - shutter
# # "\U0000e943", # 70 - shutter
# # "\U0000e944", # 80 - shutter
# # "\U0000e942", # 90 - shutter
# # "\U0000e91e", # 100 - shutter_open
# # "\U0000e91b", # music
# # "\U0000e91f", # arrow_up
# # "\U0000e920", # arrow_down
# # "\U0000e922", # vacuum
# ]

View File

@@ -0,0 +1,59 @@
esphome:
name: thread-repeater
friendly_name: Thread-repeater
min_version: 2025.9.0
api:
encryption:
key: !secret ot_ftd_led
ota:
- platform: esphome
password: !secret ota_password
esp32:
board: esp32-c6-devkitm-1
framework:
type: esp-idf
logger:
network:
enable_ipv6: true
openthread:
device_type: FTD
tlv: !secret otbr_tlv
output:
- platform: gpio
pin: GPIO15
id: light_output
light:
- platform: binary
name: "status_light"
output: light_output
text_sensor:
- platform: openthread_info
ip_address:
name: "IP Address"
channel:
name: "Channel"
role:
name: "Device Role"
rloc16:
name: "RLOC16"
ext_addr:
name: "Extended Address"
eui64:
name: "EUI64 Interface ID"
network_name:
name: "Network Name"
network_key:
name: "Network Key"
pan_id:
name: "PAN ID"
ext_pan_id:
name: "Extended PAN ID"

View File

@@ -0,0 +1,237 @@
packages:
home: !include widgets/home/home.yaml
lights_config: !include widgets/light/lights_config.yaml
devices: !include widgets/devices.yaml
settings: !include widgets/settings.yaml
menu_controls_main: !include widgets/menu_controls_main.yaml
loading_page: !include widgets/loading_page.yaml
image: !include widgets/image.yaml
font: !include widgets/fonts.yaml
color: !include widgets/colors.yaml
http_request:
verify_ssl: false
external_components:
- source: github://pr#9972
components: [mapping]
refresh: 1h
esphome:
name: display
friendly_name: display
includes:
- <sstream>
- <algorithm>
platformio_options:
board_build.flash_mode: dio
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
framework:
type: esp-idf
sdkconfig_options:
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP: y
# CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
# CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
# CONFIG_SPIRAM_FETCH_INSTRUCTIONS: y
# CONFIG_SPIRAM_RODATA: y
# CONFIG_FREERTOS_USE_TRACE_FACILITY: "y"
# CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS: "y"
psram:
mode: octal
speed: 80MHz
logger:
level: debug
api:
encryption:
key: !secret display_api
ota:
- platform: esphome
password: !secret ota_password
wifi:
networks:
- ssid: !secret wifi_ssid
password: !secret wifi_password
lvgl:
color_depth: 16
byte_order: big_endian
displays: my_display
touchscreens:
- touchscreen_id: my_touchscreen
long_press_time: 5000ms
long_press_repeat_time: 400ms
page_wrap: false
light:
# Backlight
- platform: monochromatic
output: backlight_output
name: Backlight
id: display_backlight
restore_mode: ALWAYS_ON
on_turn_on:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming by backlight on"
- lvgl.resume:
- lvgl.widget.redraw:
on_turn_off:
- if:
condition:
lambda: 'return id(display_timeout_number).state >= 0;'
then:
- logger.log: "Backlight off, pausing LVGL"
- lvgl.pause:
output:
# Backlight LED
- platform: ledc
pin: GPIO38
id: backlight_output
frequency: 100Hz
i2c:
- id: bus_a
sda: GPIO19
scl:
number: GPIO45
ignore_strapping_warning: true
frequency: 100kHz
touchscreen:
platform: gt911
id: my_touchscreen
transform:
mirror_x: false
mirror_y: false
display: my_display
on_release:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: display_backlight
# on_touch:
# - lambda: |-
# ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
# touch.x,
# touch.y,
# touch.x_raw,
# touch.y_raw
# );
spi:
- id: lcd_spi
clk_pin: GPIO48
mosi_pin: GPIO47
display:
- platform: st7701s
id: my_display
update_interval: never
auto_clear_enabled: false
data_rate: 2MHz
spi_mode: MODE3
color_order: RGB
invert_colors: false
dimensions:
width: 480
height: 480
transform:
mirror_x: false
mirror_y: false
cs_pin: 39
# reset not defined
de_pin: 18
hsync_pin: 16
vsync_pin: 17
pclk_pin: 21
init_sequence:
- 1
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10] # CMD2_BKSEL_BK0
- [0xCD, 0x00] # disable MDT flag
pclk_frequency: 12MHz
pclk_inverted: false
data_pins:
red:
- 11 # R1
- 12 # R2
- 13 # R3
- 14 # R4
- 0 # R5
green:
- 8 # G0
- 20 # G1
- 3 # G2
- 46 # G3
- 9 # G4
- 10 # G5
blue:
- 4 # B1
- 5 # B2
- 6 # B3
- 7 # B4
- 15 # B5
# interval:
# - interval: 60s
# then:
# - lambda: |-
# // Общая информация о памяти
# ESP_LOGI("memory", "Free heap: %d bytes", esp_get_free_heap_size());
# ESP_LOGI("memory", "Free internal heap: %d bytes", esp_get_free_internal_heap_size());
# ESP_LOGI("memory", "Min free heap: %d bytes", esp_get_minimum_free_heap_size());
# // Детальная статистика кучи
# multi_heap_info_t info;
# heap_caps_get_info(&info, MALLOC_CAP_INTERNAL);
# ESP_LOGI("memory", "SRAM total: %d, free: %d, largest_free: %d",
# info.total_free_bytes + info.total_allocated_bytes,
# info.total_free_bytes,
# info.largest_free_block);
# // Статистика по PSRAM
# heap_caps_get_info(&info, MALLOC_CAP_SPIRAM);
# ESP_LOGI("memory", "PSRAM total: %d, free: %d, largest_free: %d",
# info.total_free_bytes + info.total_allocated_bytes,
# info.total_free_bytes,
# info.largest_free_block);
# - lambda: |-
# ESP_LOGI("stack", "Main task free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("loopTask")));
# ESP_LOGI("stack", "System event free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("sys_evt")));
# ESP_LOGI("stack", "Timer task free: %d bytes",
# uxTaskGetStackHighWaterMark(xTaskGetHandle("esp_timer")));
# - lambda: |-
# char* buf = (char*)malloc(1024);
# if (buf) {
# vTaskList(buf);
# ESP_LOGI("stack", "\nTask Name\tState\tPrio\tStack\tNum\n%s", buf);
# free(buf);
# }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
substitutions:
device_name: "JalozieV2"
friendly_name: "JalozieV2"
comment: "esp-C6, RGBled, usbPD"
api_password: !secret JalozieV2_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
wifi_ssid2: !secret wifi_ssid2
wifi_password2: !secret wifi_password2
gateway: !secret ip_gateway
subnet: !secret ip_subnet
ip: !secret JalozieV2_ip
location: "slaapkamer"
board: "esp32-c6-devkitc-1"
framework: esp-idf
num_leds: "1"
chipset: WS2812
is_rgbw: "false"
#pins
pin_led1: GPIO05
pin_pd_cfg1: GPIO19
pin_pd_cfg2: GPIO20
pin_pd_cfg3: GPIO21
pin_mot_rst: GPIO3
pin_mot_slp: GPIO2
pin_mot_dir: GPIO1
pin_mot_stp: GPIO0
#pin_mot_en:
pin_hall_dir: GPIO6
pin_hall_stp: GPIO7
packages:
board: !include boards/esp32-gen.yaml
connection: !include common/wifi.yaml
device_base: !include common/common.yaml
logger: !include templates/logger.yaml
leds: !include templates/light_rgbw_rmt.yaml
usbcpd: !include interfaces/CH224K.yaml
stepper:
- platform: a4988
id: stepper_motor
step_pin: ${pin_mot_stp}
dir_pin: ${pin_mot_dir}
max_speed: 200
sleep_pin: ${pin_mot_slp}
acceleration: inf
deceleration: inf

View File

@@ -0,0 +1,112 @@
substitutions:
device_name: "esp32p4tablet"
friendly_name: "ESP32 P4 tablet"
comment: "esp32-P4"
api_password: !secret display_api
ota_password: !secret wifi_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
#espHosted_pins
pin_esph_reset: GPIO54
pin_esph_cmd: GPIO19
pin_esph_clk: GPIO18
pin_esph_d0: GPIO14
pin_esph_d1: GPIO15
pin_esph_d2: GPIO16
pin_esph_d3: GPIO17
#lcd_pins
pin_lcd_reset: GPIO27
pin_lcd_bl: GPIO23
pin_touch_sda: GPIO7
pin_touch_scl: GPIO8
pin_touch_rst: GPIO22
pin_touch_irq: GPIO21
packages:
board: !include boards/esp32-P4.yaml
common: !include common/common.yaml
wifi: !include common/wifi_P4.yaml
lcd: !include display/guitionJC8012P4A1.yaml
#lvgl widgets
home: !include widgets/home/home.yaml
lights_config: !include widgets/light/lights_config.yaml
devices: !include widgets/devices.yaml
settings: !include widgets/settings.yaml
menu_controls_main: !include widgets/menu_controls_main.yaml
loading_page: !include widgets/loading_page.yaml
#add includes for lvgl widgets
esphome:
includes:
- <sstream>
- <algorithm>
logger:
hardware_uart: USB_SERIAL_JTAG
level: DEBUG
logs:
lvgl: INFO
display: INFO
app: DEBUG
http_request:
verify_ssl: false
ota:
- platform: esphome
on_begin:
then:
- logger.log: "OTA gestart, LVGL pauzeren"
- lvgl.pause:
on_end:
then:
- logger.log: "OTA klaar, LVGL hervatten"
- lvgl.resume:
external_components:
- source: github://pr#9972
components: [mapping]
refresh: 1h
- source: github://willumpie82/esphome@dev
components: [mipi_dsi]
- source: github://kvj/esphome@jd9365_gsl3680
refresh: 0s
components: [gsl3680]
- source: github://esphome/esphome@2025.7.1
components: [i2c]
- source: github://youkorr/sd_image@main
components: [storage]
refresh: 1min
- source: github://youkorr/webdavbox3@main
components: [sd_mmc_card]
refresh: 10s
image: !include widgets/image.yaml
font: !include widgets/fonts.yaml
color: !include widgets/colors.yaml
# -------------------------------
# LVGL Display
# -------------------------------
lvgl:
buffer_size: 100%
byte_order: little_endian
displays: my_display
touchscreens:
- touchscreen_id: touchscreen_
long_press_time: 5000ms
long_press_repeat_time: 400ms
page_wrap: false
sd_mmc_card:
id: sd_card
clk_pin: GPIO43
cmd_pin: GPIO44
data0_pin: GPIO39
data1_pin: GPIO40
data2_pin: GPIO41
data3_pin: GPIO42
mode_1bit: false
slot: 0

View File

@@ -0,0 +1,689 @@
substitutions:
device_name: "habbit_desk"
comment: "esp32"
location: "kantoor"
api_password: !secret deskcontroller_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
gateway: !secret ip_gateway
subnet: !secret ip_subnet
# Device customization
name: habbit_desk
friendly_name: Habbit Desk
background_color: '000000'
main_icon_file: https://aguacatec.es/wp-content/uploads/2025/01/aguacatec.png
secondary_image_file1: https://aguacatec.es/wp-content/uploads/2025/01/aguacatec_youtube.png
secondary_image_file2: https://aguacatec.es/wp-content/uploads/2025/01/aguacatec_telegram.png
# Example of Sensors
sensor_temperature: sensor.temperatuur_slaapkamer_temperatuur
sensor_home_temperature: sensor.ikea_of_sweden_vindstyrka_temperatuur
sensor_home_humidity: sensor.temperatuur_slaapkamer_luchtvochtigheid
chat_entity: input_text.habbit_desk_chat
assist_entity: assist_satellite.voice_zolder_assist_satellite
assist_awake_text: "Te escucho, ¿qué necesitas?"
sensor_youtube: sensor.aguacatec_suscriptores
sensor_telegram: input_number.miembros_telegram
# Example of Lights
desk_led: light.lamp_slaapkamer_2
# Example of Thermostat
climate: climate.smart_radiator_thermostat_x
# Example of Vacuum
vacuum: vacuum.roborock_qrevo_s
# Example of Switches
printer: switch.regleta_l3
printer3d: switch.regleta_l4
# Other settings
# Otros ajustes
allowed_characters: " ¿?¡!#%'()+,-./:°0123456789ABCDEFGHIJKLMNOPQRSTUVWYZabcdefghijklmnopqrstuvwxyzáéíóú"
################################################################################################################
esphome:
name: ${name}
friendly_name: ${friendly_name}
platformio_options:
upload_speed: 921600
build_unflags: -Werror=all
board_build.flash_mode: dio
board_build.f_flash: 80000000L
board_build.f_cpu: 240000000L
psram:
mode: octal
esp32:
board: esp32-s3-devkitc-1
flash_size: 16MB
framework:
type: esp-idf
external_components:
- source: github://buglloc/esphome-components
components: [sy6970]
#disable Blinking led
sy6970:
i2c_id: touchscreen_bus
state_led_enable: false
packages:
connection: !include common/wifi.yaml
# Enable logging
logger:
globals:
- id: inactivity_time
type: int
restore_value: no
initial_value: '0'
switch:
- platform: template
name: "Auto Lock"
id: auto_lock
icon: "mdi:lock-clock"
optimistic: true
restore_mode: 'restore_default_off'
- platform: template
name: "Display"
id: habbit_display
icon: "mdi:fit-to-screen"
optimistic: true
restore_mode: 'always_on'
on_turn_on:
- light.turn_on: backlight
- lambda: |-
id(inactivity_time) = 0;
on_turn_off:
- light.turn_off: backlight
- display.page.show: home
number:
- platform: template
name: "Time Out"
id: time_out
icon: "mdi:timer-sand"
optimistic: true
min_value: 10
max_value: 600
step: 10
unit_of_measurement: "s"
restore_value: true
font:
- file: "gfonts://Space Grotesk"
id: clock_time
size: 30
glyphs: ${allowed_characters}
- file: "gfonts://Space Grotesk"
id: secondary
size: 16
glyphs: ${allowed_characters}
- file: "gfonts://Roboto"
id: info
size: 20
glyphs: ${allowed_characters}
- file: "gfonts://Roboto"
id: title
size: 25
glyphs: ${allowed_characters}
- file: "gfonts://Space Grotesk"
id: big_numbers
size: 120
glyphs: ${allowed_characters}
color:
- id: background_color
hex: ${background_color}
- id: orange
hex: 'e9c726'
- id: white
hex: 'ffffff'
- id: dirty_white
hex: 'c6c6c6'
- id: grey
hex: '222222'
- id: light_grey
hex: '444444'
- id: lime
hex: 'deff00'
- id: crimson
hex: 'f3528f'
- id: blue
hex: '52c0f3'
- id: dark_blue
hex: '1d85b6'
- id: magenta
hex: 'a91225'
- id: dark_magenta
hex: '440109'
image:
rgb:
alpha_channel:
- file: ${main_icon_file}
id: icon_habbit
resize: 180x180
# type: RGB
# transparency: alpha_channel
- file: ${secondary_image_file1}
id: secondary_image1
resize: 160x160
# type: RGB
# transparency: alpha_channel
- file: ${secondary_image_file2}
id: secondary_image2
resize: 120x120
# type: RGB
# transparency: alpha_channel
binary:
- file: mdi:chat-outline
id: icon_chat
resize: 40x40
- file: mdi:chat-processing
id: icon_assist
resize: 40x40
- file: mdi:white-balance-sunny
id: icon_weather
resize: 40x40
- file: mdi:home-thermometer
id: icon_temperature
resize: 40x40
- file: mdi:water-percent
id: icon_humidity
resize: 40x40
- file: mdi:home-automation
id: icon_devices
resize: 50x50
- file: mdi:exit-to-app
id: icon_exit
resize: 45x45
- file: mdi:plus-thick
id: icon_plus
resize: 45x45
- file: mdi:led-strip-variant
id: icon_led_strip
resize: 100x100
- file: mdi:thermostat
id: icon_thermostat
resize: 100x100
- file: mdi:robot-vacuum
id: icon_vacuum
resize: 100x100
- file: mdi:printer
id: icon_printer
resize: 100x100
- file: mdi:printer-3d-nozzle
id: icon_printer3d
resize: 100x100
# This will fetch time from Home Assistant
time:
- platform: homeassistant
id: esptime
# Create sensors from HA you want to use and show
# Crea los sensores de HA que quieres utilizar y mostrar
sensor:
- platform: homeassistant
id: sensor_temperature
entity_id: ${sensor_temperature}
internal: true
- platform: homeassistant
id: sensor_home_temperature
entity_id: ${sensor_home_temperature}
internal: true
- platform: homeassistant
id: sensor_home_humidity
entity_id: ${sensor_home_humidity}
internal: true
# - platform: homeassistant
# id: sensor_youtube
# entity_id: ${sensor_youtube}
# internal: true
# on_value:
# then:
# - display.page.show: secondary1
# - light.turn_on: backlight
# - switch.turn_on: habbit_display
# - lambda: |-
# id(inactivity_time) = 0;
# - platform: homeassistant
# id: sensor_telegram
# entity_id: ${sensor_telegram}
# internal: true
# on_value:
# then:
# - display.page.show: secondary2
# - light.turn_on: backlight
# - switch.turn_on: habbit_display
# - lambda: |-
# id(inactivity_time) = 0;
text_sensor:
# - platform: homeassistant
# id: chat_message
# entity_id: ${chat_entity}
# internal: true
# on_value:
# then:
# - display.page.show: home
# - light.turn_on: backlight
# - switch.turn_on: habbit_display
# - lambda: |-
# id(inactivity_time) = 0;
- platform: homeassistant
id: chat_assist
entity_id: ${assist_entity}
internal: true
on_value:
then:
- if:
condition:
lambda: 'return id(chat_assist).state == "listening";'
then:
- display.page.show: home
- light.turn_on: backlight
- switch.turn_on: habbit_display
- lambda: |-
id(inactivity_time) = 0;
- platform: homeassistant
id: device_desk_led
entity_id: ${desk_led}
internal: true
- platform: homeassistant
id: device_thermostat
entity_id: ${climate}
internal: true
- platform: homeassistant
id: device_vacuum
entity_id: ${vacuum}
internal: true
- platform: homeassistant
id: device_printer
entity_id: ${printer}
internal: true
- platform: homeassistant
id: device_printer3d
entity_id: ${printer3d}
internal: true
spi:
- id: display_qspi
type: quad
clk_pin: 17
data_pins:
- 13
- 18
- 21
- 14
i2c:
sda: 15
scl: 10
id: touchscreen_bus
touchscreen:
- platform: axs15231
id: main_touch
display: main_display
i2c_id: touchscreen_bus
transform:
mirror_x: true
mirror_y: False
swap_xy: false
calibration:
x_min: 0
x_max: 640
y_min: 0
y_max: 180
on_touch:
- lambda: |-
ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d, state=%d",
touch.x,
touch.y,
touch.x_raw,
touch.y_raw,
touch.state
);
- lambda: |-
id(inactivity_time) = 0;
binary_sensor:
- platform: touchscreen
name: Screen
internal: true
x_min: 0
x_max: 640
y_min: 0
y_max: 180
on_press:
then:
- lambda: |-
id(inactivity_time) = 0;
- light.turn_on: backlight
- switch.turn_on: habbit_display
- platform: touchscreen
name: Devices button
internal: true
x_min: 580
x_max: 630
y_min: 0
y_max: 90
on_press:
then:
- if:
condition:
display.is_displaying_page: home
then:
- display.page.show: devices1
else:
- display.page.show: home
#[11:04:51.630][I][cal:375]: x=608, y=29, x_raw=31, y_raw=151, state=1
- platform: touchscreen
name: More button
internal: true
x_min: 580
x_max: 630
y_min: 90
y_max: 180
on_press:
then:
- if:
condition:
display.is_displaying_page: devices1
then:
- display.page.show: devices2
# else:
# - if:
# condition:
# display.is_displaying_page: devices2
# then:
# - display.page.show: devices1
# else:
# - if:
# condition:
# display.is_displaying_page: secondary1
# then:
# - display.page.show: secondary2
# else:
# - if:
# condition:
# display.is_displaying_page: secondary2
# then:
# - display.page.show: secondary1
- platform: touchscreen
name: Device 1
internal: true
x_min: 0
x_max: 160
y_min: 0
y_max: 180
on_press:
then:
# - if:
# condition:
# - switch.is_on: habbit_display
# - display.is_displaying_page: home
# then:
# - display.page.show: secondary1
- if:
condition:
display.is_displaying_page: devices1
then:
- homeassistant.action:
service: light.toggle
data:
entity_id: ${desk_led}
- if:
condition:
display.is_displaying_page: devices2
then:
- homeassistant.action:
service: switch.toggle
data:
entity_id: ${printer3d}
- platform: touchscreen
name: Device 2
internal: true
x_min: 161
x_max: 290
y_min: 0
y_max: 180
on_press:
then:
- if:
condition:
display.is_displaying_page: devices1
then:
- homeassistant.action:
service: climate.toggle
data:
entity_id: ${climate}
- platform: touchscreen
name: Device 3
internal: true
x_min: 291
x_max: 420
y_min: 0
y_max: 180
on_press:
then:
- if:
condition:
display.is_displaying_page: devices1
then:
- if:
condition:
lambda: 'return id(device_vacuum).state == "cleaning";'
then:
- homeassistant.action:
service: vacuum.pause
data:
entity_id: ${vacuum}
else:
- homeassistant.action:
service: vacuum.start
data:
entity_id: ${vacuum}
- platform: touchscreen
name: Device 4
internal: true
x_min: 421
x_max: 550
y_min: 0
y_max: 180
on_press:
then:
- if:
condition:
display.is_displaying_page: devices1
then:
- homeassistant.action:
service: switch.toggle
data:
entity_id: ${printer}
output:
- platform: ledc
pin: GPIO1
id: backlight_pwm
light:
- platform: monochromatic
output: backlight_pwm
name: Display
id: backlight
restore_mode: ALWAYS_ON
internal: True
display:
- platform: mipi_spi
id: main_display
spi_id: display_qspi
#id: lily_display
model: AXS15231
#spi_id: lily_spi
dimensions:
height: 640 #180
width: 184 #180
cs_pin: 12
reset_pin: 16
rotation: 90
auto_clear_enabled: false
# display:
# - platform: mipi_spi
# id: main_display
# spi_id: display_qspi
# #id: lily_display
# model: AXS15231
# dimensions:
# height: 640
# width: 180
# cs_pin: 12
# reset_pin: 16
# transform:
# mirror_x: true
# mirror_y: true
# swap_xy: false
# rotation: 90
# auto_clear_enabled: false
pages:
- id: home
lambda: |-
it.fill(id(background_color));
it.image(-20, 0, id(icon_habbit));
if (id(chat_assist).state == "listening") {
it.image(100, 20, id(icon_assist), id(blue));
it.print(150, 25, id(info), "${assist_awake_text}");
}
// else {
// it.image(100, 20, id(icon_chat), id(lime));
// std::string message = id(chat_message).state.c_str();
// if (message.length() > 35) {
// message = message.substr(0, 35) + "...";
// }
// it.print(150, 25, id(info), message.c_str());
// }
it.strftime(580, 30, id(clock_time), TextAlign::CENTER, "%H:%M", id(esptime).now());
it.strftime(580, 53, id(secondary), TextAlign::CENTER, "%d/%m/%y", id(esptime).now());
it.image(170, 90, id(icon_weather), id(orange));
it.printf(220, 95, id(title), id(dirty_white), "%.0f°C", id(sensor_temperature).state);
it.image(300, 90, id(icon_temperature), id(crimson));
it.printf(355, 95, id(title), id(dirty_white), "%.0f°C", id(sensor_home_temperature).state);
it.image(430, 90, id(icon_humidity), id(blue));
it.printf(475, 95, id(title), id(dirty_white), "%.0f%%", id(sensor_home_humidity).state);
it.filled_triangle(640, 180, 640, 60, 530, 180, grey);
it.image(580, 120, id(icon_devices), id(light_grey));
- id: devices1
lambda: |-
it.fill(id(background_color));
if (id(device_desk_led).state == "on") { it.image(40, 40, id(icon_led_strip), id(orange)); }
else { it.image(40, 40, id(icon_led_strip), id(light_grey)); }
if (id(device_thermostat).state == "heat") { it.image(170, 40, id(icon_thermostat), id(crimson)); }
else { it.image(170, 40, id(icon_thermostat), id(light_grey)); }
if (id(device_vacuum).state == "cleaning") { it.image(300, 40, id(icon_vacuum), id(lime)); }
else { it.image(300, 40, id(icon_vacuum), id(light_grey)); }
if (id(device_printer).state == "on") { it.image(430, 40, id(icon_printer), id(blue)); }
else { it.image(430, 40, id(icon_printer), id(light_grey)); }
it.image(585, 10, id(icon_plus), id(light_grey));
it.filled_triangle(640, 180, 640, 60, 530, 180, grey);
it.image(585, 125, id(icon_exit), id(light_grey));
- id: devices2
lambda: |-
it.fill(id(background_color));
if (id(device_printer3d).state == "on") { it.image(40, 40, id(icon_printer3d), id(lime)); }
else { it.image(40, 40, id(icon_printer3d), id(light_grey)); }
it.image(585, 10, id(icon_plus), id(light_grey));
it.filled_triangle(640, 180, 640, 60, 530, 180, grey);
it.image(585, 125, id(icon_exit), id(light_grey));
# - id: secondary1
# lambda: |-
# it.fill(id(magenta));
# it.image(-10, 10, id(secondary_image1));
# it.printf(170, 15, id(big_numbers), id(dirty_white), "%.3f", id(sensor_youtube).state / 1000);
# it.image(585, 10, id(icon_plus), id(dark_magenta));
# it.filled_triangle(640, 180, 640, 60, 530, 180, dark_magenta);
# it.image(585, 125, id(icon_exit), id(magenta));
# - id: secondary2
# lambda: |-
# it.fill(id(blue));
# it.image(20, 30, id(secondary_image2));
# it.printf(170, 15, id(big_numbers), id(white), "%.3f", id(sensor_telegram).state / 1000);
# it.image(585, 10, id(icon_plus), id(dark_blue));
# it.filled_triangle(640, 180, 640, 60, 530, 180, dark_blue);
# it.image(585, 125, id(icon_exit), id(blue));
interval:
- interval: 1s
then:
- lambda: |-
if (id(auto_lock).state) {
if (id(inactivity_time) < id(time_out).state) {
id(inactivity_time) += 1;
} else {
id(backlight_pwm).turn_off();
id(habbit_display).turn_off();
}
}

View File

@@ -0,0 +1,3 @@
api:
encryption:
key: ${api_password}

View File

@@ -0,0 +1,4 @@
ota:
- platform: esphome
id: my_ota
password: ${ota_password}

View File

@@ -0,0 +1,29 @@
sensor:
- platform: wifi_signal
name: "WiFi Signal "
update_interval: 10s
id: wifisignal
text_sensor:
- platform: wifi_info
ip_address:
name: IP Address
icon: mdi:wifi-strength-2
id: ipaddr
ssid:
name: "Connected SSID"
id: ssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
bssid:
name: "Connected BSSID"
id: bssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
mac_address:
name: "WiFi Mac Address"
id: macaddress
icon: mdi:wifi-strength-2
entity_category: diagnostic

View File

@@ -0,0 +1,19 @@
wifi:
networks:
- ssid: ${wifi_ssid}
password: ${wifi_password}
# manual_ip:
# static_ip: ${ip}
# gateway: ${gateway}
# subnet: ${subnet}
# dns1: 192.169.2.15
# dns2: 1.1.1.1
# - ssid: ${wifi_ssid2}
# password: ${wifi_password2}
#use_address: 192.168.2.235 #when changing fixed IP
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: ${device_name}
password: ${wifi_password}

View File

@@ -1,18 +0,0 @@
ota:
- platform: esphome
id: my_ota
password: ${ota_password}
api:
encryption:
key: ${api_password}
text_sensor:
- platform: ethernet_info
ip_address:
name: ESP IP Address
icon: mdi:ethernet
entity_category: diagnostic

View File

@@ -1,61 +1,7 @@
packages:
api: !include connection/api.yaml
ota: !include connection/ota.yaml
wifi: !include connection/wifi.yaml
sensor: !include connection/wifi-sensor.yaml
# Enable Home Assistant API
api:
encryption:
key: ${api_password}
ota:
- platform: esphome
id: my_ota
password: ${ota_password}
wifi:
networks:
- ssid: ${wifi_ssid}
password: ${wifi_password}
manual_ip:
static_ip: ${ip}
gateway: ${gateway}
subnet: ${subnet}
dns1: 192.169.2.15
dns2: 1.1.1.1
- ssid: ${wifi_ssid2}
password: ${wifi_password2}
#use_address: 192.168.2.235 #when changing fixed IP
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: ${device_name}
password: ${wifi_password}
captive_portal:
sensor:
- platform: wifi_signal
name: "WiFi Signal "
update_interval: 10s
text_sensor:
- platform: wifi_info
ip_address:
name: IP Address
icon: mdi:wifi-strength-2
id: ipaddr
ssid:
name: "Connected SSID"
id: ssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
bssid:
name: "Connected BSSID"
id: bssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
mac_address:
name: "WiFi Mac Address"
id: macaddress
icon: mdi:wifi-strength-2
entity_category: diagnostic
captive_portal:

View File

@@ -0,0 +1,19 @@
packages:
api: !include connection/api.yaml
ota: !include connection/ota.yaml
wifi: !include connection/wifi.yaml
#sensor: !include connection/wifi-sensor.yaml
esp32_hosted:
variant: ESP32C6
reset_pin: ${pin_esph_reset}
cmd_pin: ${pin_esph_cmd}
clk_pin: ${pin_esph_clk}
d0_pin: ${pin_esph_d0}
d1_pin: ${pin_esph_d1}
d2_pin: ${pin_esph_d2}
d3_pin: ${pin_esph_d3}
active_high: true
captive_portal:

View File

@@ -1,60 +1,14 @@
# Enable Home Assistant API
api:
encryption:
key: ${api_password}
ota:
- platform: esphome
id: my_ota
password: ${ota_password}
!include templates/api.yaml
!include templates/ota.yaml
!include templates/wifi.yaml
!include sensors/wifi.yaml
wifi:
networks:
- ssid: ${wifi_ssid}
password: ${wifi_password}
manual_ip:
static_ip: ${ip}
gateway: ${gateway}
subnet: ${subnet}
dns1: 192.169.2.15
dns2: 1.1.1.1
- ssid: ${wifi_ssid2}
password: ${wifi_password2}
# use_address: 192.168.2.63 #when changing fixed IP
ap:
ssid: ${device_name}
password: ${wifi_password}
on_connect:
- delay: 5s # Gives time for improv results to be transmitted
- ble.disable:
on_disconnect:
- ble.enable:
sensor:
- platform: wifi_signal
name: "WiFi Signal "
update_interval: 10s
text_sensor:
- platform: wifi_info
ssid:
name: "Connected SSID"
id: ssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
bssid:
name: "Connected BSSID"
id: bssid
icon: mdi:wifi-strength-2
entity_category: diagnostic
mac_address:
name: "WiFi Mac Address"
id: macaddress
icon: mdi:wifi-strength-2
entity_category: diagnostic

View File

@@ -1,29 +1,6 @@
api:
encryption:
key: ${api_password}
ota:
- platform: esphome
id: my_ota
password: ${ota_password}
wifi:
networks:
- ssid: ${wifi_ssid}
password: ${wifi_password}
manual_ip:
static_ip: ${ip}
gateway: ${gateway}
subnet: ${subnet}
dns1: 192.169.2.15
dns2: 1.1.1.1
- ssid: ${wifi_ssid2}
password: ${wifi_password2}
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: ${device_name}
password: ${wifi_password}
packages:
api: !include connection/api.yaml
ota: !include connection/ota.yaml
wifi: !include connection/wifi.yaml
captive_portal:

View File

@@ -0,0 +1,224 @@
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_FROM,
CONF_ID,
CONF_LAMBDA,
CONF_PAGE_ID,
CONF_PAGES,
CONF_ROTATION,
CONF_TO,
CONF_TRIGGER_ID,
CONF_UPDATE_INTERVAL,
SCHEDULER_DONT_RUN,
)
from esphome.core import CoroPriority, coroutine_with_priority
IS_PLATFORM_COMPONENT = True
display_ns = cg.esphome_ns.namespace("display")
Display = display_ns.class_("Display", cg.PollingComponent)
DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
DisplayPage = display_ns.class_("DisplayPage")
DisplayPagePtr = DisplayPage.operator("ptr")
DisplayRef = Display.operator("ref")
DisplayPageShowAction = display_ns.class_("DisplayPageShowAction", automation.Action)
DisplayPageShowNextAction = display_ns.class_(
"DisplayPageShowNextAction", automation.Action
)
DisplayPageShowPrevAction = display_ns.class_(
"DisplayPageShowPrevAction", automation.Action
)
DisplayIsDisplayingPageCondition = display_ns.class_(
"DisplayIsDisplayingPageCondition", automation.Condition
)
DisplayOnPageChangeTrigger = display_ns.class_(
"DisplayOnPageChangeTrigger", automation.Trigger
)
CONF_ON_PAGE_CHANGE = "on_page_change"
CONF_SHOW_TEST_CARD = "show_test_card"
CONF_UNSPECIFIED = "unspecified"
DISPLAY_ROTATIONS = {
0: display_ns.DISPLAY_ROTATION_0_DEGREES,
90: display_ns.DISPLAY_ROTATION_90_DEGREES,
180: display_ns.DISPLAY_ROTATION_180_DEGREES,
270: display_ns.DISPLAY_ROTATION_270_DEGREES,
}
def validate_rotation(value):
value = cv.string(value)
value = value.removesuffix("°")
return cv.enum(DISPLAY_ROTATIONS, int=True)(value)
def validate_auto_clear(value):
if value == CONF_UNSPECIFIED:
return value
return cv.boolean(value)
BASIC_DISPLAY_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
}
).extend(cv.polling_component_schema("1s"))
def _validate_test_card(config):
if (
config.get(CONF_SHOW_TEST_CARD, False)
and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
):
raise cv.Invalid(
f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card."
)
return config
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
{
cv.Optional(CONF_ROTATION): validate_rotation,
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
cv.ensure_list(
{
cv.GenerateID(): cv.declare_id(DisplayPage),
cv.Required(CONF_LAMBDA): cv.lambda_,
}
),
cv.Length(min=1),
),
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayOnPageChangeTrigger
),
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
}
),
cv.Optional(
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
): validate_auto_clear,
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
}
)
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):
if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
# the possible schemas are too complex to do this easily.
if auto_clear == CONF_UNSPECIFIED:
auto_clear = CONF_LAMBDA in config or CONF_PAGES in config
cg.add(var.set_auto_clear(auto_clear))
if CONF_PAGES in config:
pages = []
for conf in config[CONF_PAGES]:
lambda_ = await cg.process_lambda(
conf[CONF_LAMBDA], [(DisplayRef, "it")], return_type=cg.void
)
page = cg.new_Pvariable(conf[CONF_ID], lambda_)
pages.append(page)
cg.add(var.set_pages(pages))
for conf in config.get(CONF_ON_PAGE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
if CONF_FROM in conf:
page = await cg.get_variable(conf[CONF_FROM])
cg.add(trigger.set_from(page))
if CONF_TO in conf:
page = await cg.get_variable(conf[CONF_TO])
cg.add(trigger.set_to(page))
await automation.build_automation(
trigger, [(DisplayPagePtr, "from"), (DisplayPagePtr, "to")], conf
)
if config.get(CONF_SHOW_TEST_CARD):
cg.add(var.show_test_card())
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
@automation.register_action(
"display.page.show",
DisplayPageShowAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayPage)),
}
),
)
async def display_page_show_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
if isinstance(config[CONF_ID], core.Lambda):
template_ = await cg.templatable(config[CONF_ID], args, DisplayPagePtr)
cg.add(var.set_page(template_))
else:
paren = await cg.get_variable(config[CONF_ID])
cg.add(var.set_page(paren))
return var
@automation.register_action(
"display.page.show_next",
DisplayPageShowNextAction,
maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
)
async def display_page_show_next_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"display.page.show_previous",
DisplayPageShowPrevAction,
maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)),
}
),
)
async def display_page_show_previous_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition(
"display.is_displaying_page",
DisplayIsDisplayingPageCondition,
cv.maybe_simple_value(
{
cv.GenerateID(CONF_ID): cv.use_id(Display),
cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage),
},
key=CONF_PAGE_ID,
),
)
async def display_is_displaying_page_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
page = await cg.get_variable(config[CONF_PAGE_ID])
var = cg.new_Pvariable(condition_id, template_arg, paren)
cg.add(var.set_page(page))
return var
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config):
cg.add_global(display_ns.using)
cg.add_define("USE_DISPLAY")

View File

@@ -0,0 +1,870 @@
#include "display.h"
#include <utility>
#include <numbers>
#include "display_color_utils.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
const Color COLOR_OFF(0, 0, 0, 0);
const Color COLOR_ON(255, 255, 255, 255);
void Display::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); }
void Display::clear() { this->fill(COLOR_OFF); }
void Display::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; }
void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1;
const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1;
int32_t err = dx + dy;
while (true) {
this->draw_pixel_at(x1, y1, color);
if (x1 == x2 && y1 == y2)
break;
int32_t e2 = 2 * err;
if (e2 >= dy) {
err += dy;
x1 += sx;
}
if (e2 <= dx) {
err += dx;
y1 += sy;
}
}
}
void Display::line_at_angle(int x, int y, int angle, int length, Color color) {
this->line_at_angle(x, y, angle, 0, length, color);
}
void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color) {
// Calculate start and end points
int x1 = (start_radius * cos(angle * M_PI / 180)) + x;
int y1 = (start_radius * sin(angle * M_PI / 180)) + y;
int x2 = (stop_radius * cos(angle * M_PI / 180)) + x;
int y2 = (stop_radius * sin(angle * M_PI / 180)) + y;
// Draw line
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
}
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++)
this->draw_pixel_at(i, y, color);
}
void HOT Display::vertical_line(int x, int y, int height, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = y; i < y + height; i++)
this->draw_pixel_at(x, i, color);
}
void Display::rectangle(int x1, int y1, int width, int height, Color color) {
this->horizontal_line(x1, y1, width, color);
this->horizontal_line(x1, y1 + height - 1, width, color);
this->vertical_line(x1, y1, height, color);
this->vertical_line(x1 + width - 1, y1, height, color);
}
void Display::filled_rectangle(int x1, int y1, int width, int height, Color color) {
// Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses.
for (int i = y1; i < y1 + height; i++) {
this->horizontal_line(x1, i, width, color);
}
}
void HOT Display::circle(int center_x, int center_xy, int radius, Color color) {
int dx = -radius;
int dy = 0;
int err = 2 - 2 * radius;
int e2;
do {
this->draw_pixel_at(center_x - dx, center_xy + dy, color);
this->draw_pixel_at(center_x + dx, center_xy + dy, color);
this->draw_pixel_at(center_x + dx, center_xy - dy, color);
this->draw_pixel_at(center_x - dx, center_xy - dy, color);
e2 = err;
if (e2 < dy) {
err += ++dy * 2 + 1;
if (-dx == dy && e2 <= dx) {
e2 = 0;
}
}
if (e2 > dx) {
err += ++dx * 2 + 1;
}
} while (dx <= 0);
}
void Display::filled_circle(int center_x, int center_y, int radius, Color color) {
int dx = -int32_t(radius);
int dy = 0;
int err = 2 - 2 * radius;
int e2;
do {
this->draw_pixel_at(center_x - dx, center_y + dy, color);
this->draw_pixel_at(center_x + dx, center_y + dy, color);
this->draw_pixel_at(center_x + dx, center_y - dy, color);
this->draw_pixel_at(center_x - dx, center_y - dy, color);
int hline_width = 2 * (-dx) + 1;
this->horizontal_line(center_x + dx, center_y + dy, hline_width, color);
this->horizontal_line(center_x + dx, center_y - dy, hline_width, color);
e2 = err;
if (e2 < dy) {
err += ++dy * 2 + 1;
if (-dx == dy && e2 <= dx) {
e2 = 0;
}
}
if (e2 > dx) {
err += ++dx * 2 + 1;
}
} while (dx <= 0);
}
void Display::filled_ring(int center_x, int center_y, int radius1, int radius2, Color color) {
int rmax = radius1 > radius2 ? radius1 : radius2;
int rmin = radius1 < radius2 ? radius1 : radius2;
int dxmax = -int32_t(rmax), dxmin = -int32_t(rmin);
int dymax = 0, dymin = 0;
int errmax = 2 - 2 * rmax, errmin = 2 - 2 * rmin;
int e2max, e2min;
do {
// 8 dots for borders
this->draw_pixel_at(center_x - dxmax, center_y + dymax, color);
this->draw_pixel_at(center_x + dxmax, center_y + dymax, color);
this->draw_pixel_at(center_x - dxmin, center_y + dymin, color);
this->draw_pixel_at(center_x + dxmin, center_y + dymin, color);
this->draw_pixel_at(center_x + dxmax, center_y - dymax, color);
this->draw_pixel_at(center_x - dxmax, center_y - dymax, color);
this->draw_pixel_at(center_x + dxmin, center_y - dymin, color);
this->draw_pixel_at(center_x - dxmin, center_y - dymin, color);
if (dymin < rmin) {
// two parts - four lines
int hline_width = -(dxmax - dxmin) + 1;
this->horizontal_line(center_x + dxmax, center_y + dymax, hline_width, color);
this->horizontal_line(center_x - dxmin, center_y + dymax, hline_width, color);
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
this->horizontal_line(center_x - dxmin, center_y - dymax, hline_width, color);
} else {
// one part - top and bottom
int hline_width = 2 * (-dxmax) + 1;
this->horizontal_line(center_x + dxmax, center_y + dymax, hline_width, color);
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
}
e2max = errmax;
// tune external
if (e2max < dymax) {
errmax += ++dymax * 2 + 1;
if (-dxmax == dymax && e2max <= dxmax) {
e2max = 0;
}
}
if (e2max > dxmax) {
errmax += ++dxmax * 2 + 1;
}
// tune internal
while (dymin < dymax && dymin < rmin) {
e2min = errmin;
if (e2min < dymin) {
errmin += ++dymin * 2 + 1;
if (-dxmin == dymin && e2min <= dxmin) {
e2min = 0;
}
}
if (e2min > dxmin) {
errmin += ++dxmin * 2 + 1;
}
}
} while (dxmax <= 0);
}
void Display::filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color) {
int rmax = radius1 > radius2 ? radius1 : radius2;
int rmin = radius1 < radius2 ? radius1 : radius2;
int dxmax = -int32_t(rmax), dxmin = -int32_t(rmin), upd_dxmax, upd_dxmin;
int dymax = 0, dymin = 0;
int errmax = 2 - 2 * rmax, errmin = 2 - 2 * rmin;
int e2max, e2min;
progress = std::max(0, std::min(progress, 100)); // 0..100
int draw_progress = progress > 50 ? (100 - progress) : progress;
float tan_a = (progress == 50) ? 65535 : tan(float(draw_progress) * M_PI / 100); // slope
do {
// outer dots
this->draw_pixel_at(center_x + dxmax, center_y - dymax, color);
this->draw_pixel_at(center_x - dxmax, center_y - dymax, color);
if (dymin < rmin) { // side parts
int lhline_width = -(dxmax - dxmin) + 1;
if (progress >= 50) {
if (float(dymax) < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a);
} else {
upd_dxmax = -dxmax;
}
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color); // left
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
if (upd_dxmax > -dxmin) { // right
int rhline_width = (upd_dxmax + dxmin) + 1;
this->horizontal_line(center_x - dxmin, center_y - dymax,
rhline_width > lhline_width ? lhline_width : rhline_width, color);
}
} else {
if (float(dymin) > float(-dxmin) * tan_a) {
upd_dxmin = ceil(float(dymin) / tan_a);
} else {
upd_dxmin = -dxmin;
}
lhline_width = -(dxmax + upd_dxmin) + 1;
if (!dymax)
this->horizontal_line(center_x - dxmin, center_y, lhline_width, color); // right horizontal border
if (lhline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, lhline_width, color);
}
} else { // top part
int hline_width = 2 * (-dxmax) + 1;
if (progress >= 50) {
if (dymax < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a);
hline_width = -dxmax + upd_dxmax + 1;
}
} else {
if (dymax < float(-dxmax) * tan_a) {
upd_dxmax = ceil(float(dymax) / tan_a);
hline_width = -dxmax - upd_dxmax + 1;
} else {
hline_width = 0;
}
}
if (hline_width > 0)
this->horizontal_line(center_x + dxmax, center_y - dymax, hline_width, color);
}
e2max = errmax;
if (e2max < dymax) {
errmax += ++dymax * 2 + 1;
if (-dxmax == dymax && e2max <= dxmax) {
e2max = 0;
}
}
if (e2max > dxmax) {
errmax += ++dxmax * 2 + 1;
}
while (dymin <= dymax && dymin <= rmin && dxmin <= 0) {
this->draw_pixel_at(center_x + dxmin, center_y - dymin, color);
this->draw_pixel_at(center_x - dxmin, center_y - dymin, color);
e2min = errmin;
if (e2min < dymin) {
errmin += ++dymin * 2 + 1;
if (-dxmin == dymin && e2min <= dxmin) {
e2min = 0;
}
}
if (e2min > dxmin) {
errmin += ++dxmin * 2 + 1;
}
}
} while (dxmax <= 0);
}
void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
this->line(x1, y1, x2, y2, color);
this->line(x1, y1, x3, y3, color);
this->line(x2, y2, x3, y3, color);
}
void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) {
if (*y1 > *y2) {
int x_temp = *x1, y_temp = *y1;
*x1 = *x2, *y1 = *y2;
*x2 = x_temp, *y2 = y_temp;
}
if (*y1 > *y3) {
int x_temp = *x1, y_temp = *y1;
*x1 = *x3, *y1 = *y3;
*x3 = x_temp, *y3 = y_temp;
}
if (*y2 > *y3) {
int x_temp = *x2, y_temp = *y2;
*x2 = *x3, *y2 = *y3;
*x3 = x_temp, *y3 = y_temp;
}
}
void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// y2 must be equal to y3 (same horizontal line)
// Initialize Bresenham's algorithm for side 1
int s1_current_x = x1;
int s1_current_y = y1;
bool s1_axis_swap = false;
int s1_dx = abs(x2 - x1);
int s1_dy = abs(y2 - y1);
int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1;
int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1;
if (s1_dy > s1_dx) { // swap values
int tmp = s1_dx;
s1_dx = s1_dy;
s1_dy = tmp;
s1_axis_swap = true;
}
int s1_error = 2 * s1_dy - s1_dx;
// Initialize Bresenham's algorithm for side 2
int s2_current_x = x1;
int s2_current_y = y1;
bool s2_axis_swap = false;
int s2_dx = abs(x3 - x1);
int s2_dy = abs(y3 - y1);
int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1;
int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1;
if (s2_dy > s2_dx) { // swap values
int tmp = s2_dx;
s2_dx = s2_dy;
s2_dy = tmp;
s2_axis_swap = true;
}
int s2_error = 2 * s2_dy - s2_dx;
// Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis.
for (int i = 0; i <= s1_dx; i++) {
if (s1_current_x <= s2_current_x) {
this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color);
} else {
this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color);
}
// Bresenham's #1
// Side 1 s1_current_x and s1_current_y calculation
while (s1_error >= 0) {
if (s1_axis_swap) {
s1_current_x += s1_sign_x;
} else {
s1_current_y += s1_sign_y;
}
s1_error = s1_error - 2 * s1_dx;
}
if (s1_axis_swap) {
s1_current_y += s1_sign_y;
} else {
s1_current_x += s1_sign_x;
}
s1_error = s1_error + 2 * s1_dy;
// Bresenham's #2
// Side 2 s2_current_x and s2_current_y calculation
while (s2_current_y != s1_current_y) {
while (s2_error >= 0) {
if (s2_axis_swap) {
s2_current_x += s2_sign_x;
} else {
s2_current_y += s2_sign_y;
}
s2_error = s2_error - 2 * s2_dx;
}
if (s2_axis_swap) {
s2_current_y += s2_sign_y;
} else {
s2_current_x += s2_sign_x;
}
s2_error = s2_error + 2 * s2_dy;
}
}
}
void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
// Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point
this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3);
if (y2 == y3) { // Check for special case of a bottom-flat triangle
this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color);
} else if (y1 == y2) { // Check for special case of a top-flat triangle
this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color);
} else { // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2;
this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color);
this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
}
}
void HOT Display::get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y,
int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees) {
if (edges >= 2) {
// Given the orientation of the display component, an angle is measured clockwise from the x axis.
// For a regular polygon, the human reference would be the top of the polygon,
// hence we rotate the shape by 270° to orient the polygon up.
rotation_degrees += ROTATION_270_DEGREES;
// Convert the rotation to radians, easier to use in trigonometrical calculations
float rotation_radians = rotation_degrees * std::numbers::pi / 180;
// A pointy top variation means the first vertex of the polygon is at the top center of the shape, this requires no
// additional rotation of the shape.
// A flat top variation means the first point of the polygon has to be rotated so that the first edge is horizontal,
// this requires to rotate the shape by π/edges radians counter-clockwise so that the first point is located on the
// left side of the first horizontal edge.
rotation_radians -= (variation == VARIATION_FLAT_TOP) ? std::numbers::pi / edges : 0.0;
float vertex_angle = ((float) vertex_id) / edges * 2 * std::numbers::pi + rotation_radians;
*vertex_x = (int) round(cos(vertex_angle) * radius) + center_x;
*vertex_y = (int) round(sin(vertex_angle) * radius) + center_y;
}
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees, Color color, RegularPolygonDrawing drawing) {
if (edges >= 2) {
int previous_vertex_x, previous_vertex_y;
for (int current_vertex_id = 0; current_vertex_id <= edges; current_vertex_id++) {
int current_vertex_x, current_vertex_y;
get_regular_polygon_vertex(current_vertex_id, &current_vertex_x, &current_vertex_y, x, y, radius, edges,
variation, rotation_degrees);
if (current_vertex_id > 0) { // Start drawing after the 2nd vertex coordinates has been calculated
if (drawing == DRAWING_FILLED) {
this->filled_triangle(x, y, previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color);
} else if (drawing == DRAWING_OUTLINE) {
this->line(previous_vertex_x, previous_vertex_y, current_vertex_x, current_vertex_y, color);
}
}
previous_vertex_x = current_vertex_x;
previous_vertex_y = current_vertex_y;
}
}
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color,
RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, drawing);
}
void HOT Display::regular_polygon(int x, int y, int radius, int edges, Color color, RegularPolygonDrawing drawing) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, drawing);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
float rotation_degrees, Color color) {
regular_polygon(x, y, radius, edges, variation, rotation_degrees, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation,
Color color) {
regular_polygon(x, y, radius, edges, variation, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
void Display::filled_regular_polygon(int x, int y, int radius, int edges, Color color) {
regular_polygon(x, y, radius, edges, VARIATION_POINTY_TOP, ROTATION_0_DEGREES, color, DRAWING_FILLED);
}
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text, Color background) {
int x_start, y_start;
int width, height;
this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height);
font->print(x_start, y_start, this, color, text, background);
}
void Display::vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
va_list arg) {
char buffer[256];
int ret = vsnprintf(buffer, sizeof(buffer), format, arg);
if (ret > 0)
this->print(x, y, font, color, align, buffer, background);
}
void Display::image(int x, int y, BaseImage *image, Color color_on, Color color_off) {
this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off);
}
void Display::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT)));
auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT)));
switch (x_align) {
case ImageAlign::RIGHT:
x -= image->get_width();
break;
case ImageAlign::CENTER_HORIZONTAL:
x -= image->get_width() / 2;
break;
case ImageAlign::LEFT:
default:
break;
}
switch (y_align) {
case ImageAlign::BOTTOM:
y -= image->get_height();
break;
case ImageAlign::CENTER_VERTICAL:
y -= image->get_height() / 2;
break;
case ImageAlign::TOP:
default:
break;
}
image->draw(x, y, this, color_on, color_off);
}
#ifdef USE_GRAPH
void Display::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
void Display::legend(int x, int y, graph::Graph *graph, Color color_on) { graph->draw_legend(this, x, y, color_on); }
#endif // USE_GRAPH
#ifdef USE_QR_CODE
void Display::qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on, int scale) {
qr_code->draw(this, x, y, color_on, scale);
}
#endif // USE_QR_CODE
#ifdef USE_GRAPHICAL_DISPLAY_MENU
void Display::menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height) {
Rect rect(x, y, width, height);
menu->draw(this, &rect);
}
#endif // USE_GRAPHICAL_DISPLAY_MENU
void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1,
int *width, int *height) {
int x_offset, baseline;
font->measure(text, width, &x_offset, &baseline, height);
auto x_align = TextAlign(int(align) & 0x18);
auto y_align = TextAlign(int(align) & 0x07);
switch (x_align) {
case TextAlign::RIGHT:
*x1 = x - *width - x_offset;
break;
case TextAlign::CENTER_HORIZONTAL:
*x1 = x - (*width + x_offset) / 2;
break;
case TextAlign::LEFT:
default:
// LEFT
*x1 = x;
break;
}
switch (y_align) {
case TextAlign::BOTTOM:
*y1 = y - *height;
break;
case TextAlign::BASELINE:
*y1 = y - baseline;
break;
case TextAlign::CENTER_VERTICAL:
*y1 = y - (*height) / 2;
break;
case TextAlign::TOP:
default:
*y1 = y;
break;
}
}
void Display::print(int x, int y, BaseFont *font, Color color, const char *text, Color background) {
this->print(x, y, font, color, TextAlign::TOP_LEFT, text, background);
}
void Display::print(int x, int y, BaseFont *font, TextAlign align, const char *text) {
this->print(x, y, font, COLOR_ON, align, text);
}
void Display::print(int x, int y, BaseFont *font, const char *text) {
this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text);
}
void Display::printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, background, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, COLOR_OFF, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, Color color, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, align, format, arg);
va_end(arg);
}
void Display::printf(int x, int y, BaseFont *font, const char *format, ...) {
va_list arg;
va_start(arg, format);
this->vprintf_(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, arg);
va_end(arg);
}
void Display::set_writer(display_writer_t &&writer) { this->writer_ = writer; }
void Display::set_pages(std::vector<DisplayPage *> pages) {
for (auto *page : pages)
page->set_parent(this);
for (uint32_t i = 0; i < pages.size() - 1; i++) {
pages[i]->set_next(pages[i + 1]);
pages[i + 1]->set_prev(pages[i]);
}
pages[0]->set_prev(pages[pages.size() - 1]);
pages[pages.size() - 1]->set_next(pages[0]);
this->show_page(pages[0]);
}
void Display::show_page(DisplayPage *page) {
this->previous_page_ = this->page_;
this->page_ = page;
if (this->previous_page_ != this->page_) {
for (auto *t : on_page_change_triggers_)
t->process(this->previous_page_, this->page_);
}
}
void Display::show_next_page() { this->page_->show_next(); }
void Display::show_prev_page() { this->page_->show_prev(); }
void Display::do_update_() {
if (this->auto_clear_enabled_) {
this->clear();
}
if (this->show_test_card_) {
this->test_card();
} else if (this->page_ != nullptr) {
this->page_->get_writer()(*this);
} else if (this->writer_.has_value()) {
(*this->writer_)(*this);
}
this->clear_clipping_();
}
void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) {
if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to))
this->trigger(from, to);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
ESPTime time) {
char buffer[64];
size_t ret = time.strftime(buffer, sizeof(buffer), format);
if (ret > 0)
this->print(x, y, font, color, align, buffer, background);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, align, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time) {
this->strftime(x, y, font, color, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, COLOR_OFF, align, format, time);
}
void Display::strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) {
this->strftime(x, y, font, COLOR_ON, COLOR_OFF, TextAlign::TOP_LEFT, format, time);
}
void Display::start_clipping(Rect rect) {
if (!this->clipping_rectangle_.empty()) {
Rect r = this->clipping_rectangle_.back();
rect.shrink(r);
}
this->clipping_rectangle_.push_back(rect);
}
void Display::end_clipping() {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "clear: Clipping is not set.");
} else {
this->clipping_rectangle_.pop_back();
}
}
void Display::extend_clipping(Rect add_rect) {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "add: Clipping is not set.");
} else {
this->clipping_rectangle_.back().extend(add_rect);
}
}
void Display::shrink_clipping(Rect add_rect) {
if (this->clipping_rectangle_.empty()) {
ESP_LOGE(TAG, "add: Clipping is not set.");
} else {
this->clipping_rectangle_.back().shrink(add_rect);
}
}
Rect Display::get_clipping() const {
if (this->clipping_rectangle_.empty()) {
return Rect();
} else {
return this->clipping_rectangle_.back();
}
}
void Display::clear_clipping_() { this->clipping_rectangle_.clear(); }
bool Display::clip(int x, int y) {
if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height())
return false;
if (!this->get_clipping().inside(x, y))
return false;
return true;
}
bool Display::clamp_x_(int x, int w, int &min_x, int &max_x) {
min_x = std::max(x, 0);
max_x = std::min(x + w, this->get_width());
if (!this->clipping_rectangle_.empty()) {
const auto &rect = this->clipping_rectangle_.back();
if (!rect.is_set())
return false;
min_x = std::max(min_x, (int) rect.x);
max_x = std::min(max_x, (int) rect.x2());
}
return min_x < max_x;
}
bool Display::clamp_y_(int y, int h, int &min_y, int &max_y) {
min_y = std::max(y, 0);
max_y = std::min(y + h, this->get_height());
if (!this->clipping_rectangle_.empty()) {
const auto &rect = this->clipping_rectangle_.back();
if (!rect.is_set())
return false;
min_y = std::max(min_y, (int) rect.y);
max_y = std::min(max_y, (int) rect.y2());
}
return min_y < max_y;
}
const uint8_t TESTCARD_FONT[3][8] PROGMEM = {{0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R'
{0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G'
{0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}}; // 'B'
void Display::test_card() {
int w = get_width(), h = get_height(), image_w, image_h;
this->clear();
this->show_test_card_ = false;
if (this->get_display_type() == DISPLAY_TYPE_COLOR) {
Color r(255, 0, 0), g(0, 255, 0), b(0, 0, 255);
image_w = std::min(w - 20, 310);
image_h = std::min(h - 20, 255);
int shift_x = (w - image_w) / 2;
int shift_y = (h - image_h) / 2;
int line_w = (image_w - 6) / 6;
int image_c = image_w / 2;
for (auto i = 0; i <= image_h; i++) {
int c = esp_scale(i, image_h);
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
this->horizontal_line(shift_x + image_c - line_w, shift_y + i, line_w, g.fade_to_white(c));
this->horizontal_line(shift_x + image_c, shift_y + i, line_w, g.fade_to_black(c));
this->horizontal_line(shift_x + image_w - (line_w * 2), shift_y + i, line_w, b.fade_to_white(c));
this->horizontal_line(shift_x + image_w - line_w, shift_y + i, line_w, b.fade_to_black(c));
}
this->rectangle(shift_x, shift_y, image_w, image_h, Color(127, 127, 0));
uint16_t shift_r = shift_x + line_w - (8 * 3);
uint16_t shift_g = shift_x + image_c - (8 * 3);
uint16_t shift_b = shift_x + image_w - line_w - (8 * 3);
shift_y = h / 2 - (8 * 3);
for (auto i = 0; i < 8; i++) {
uint8_t ftr = progmem_read_byte(&TESTCARD_FONT[0][i]);
uint8_t ftg = progmem_read_byte(&TESTCARD_FONT[1][i]);
uint8_t ftb = progmem_read_byte(&TESTCARD_FONT[2][i]);
for (auto k = 0; k < 8; k++) {
if ((ftr & (1 << k)) != 0) {
this->filled_rectangle(shift_r + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftg & (1 << k)) != 0) {
this->filled_rectangle(shift_g + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
if ((ftb & (1 << k)) != 0) {
this->filled_rectangle(shift_b + (i * 6), shift_y + (k * 6), 6, 6, COLOR_OFF);
}
}
}
}
this->rectangle(0, 0, w, h, Color(127, 0, 127));
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
this->stop_poller();
}
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() {
if (this->next_ == nullptr) {
ESP_LOGE(TAG, "no next page");
return;
}
this->next_->show();
}
void DisplayPage::show_prev() {
if (this->prev_ == nullptr) {
ESP_LOGE(TAG, "no previous page");
return;
}
this->prev_->show();
}
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
const LogString *text_align_to_string(TextAlign textalign) {
switch (textalign) {
case TextAlign::TOP_LEFT:
return LOG_STR("TOP_LEFT");
case TextAlign::TOP_CENTER:
return LOG_STR("TOP_CENTER");
case TextAlign::TOP_RIGHT:
return LOG_STR("TOP_RIGHT");
case TextAlign::CENTER_LEFT:
return LOG_STR("CENTER_LEFT");
case TextAlign::CENTER:
return LOG_STR("CENTER");
case TextAlign::CENTER_RIGHT:
return LOG_STR("CENTER_RIGHT");
case TextAlign::BASELINE_LEFT:
return LOG_STR("BASELINE_LEFT");
case TextAlign::BASELINE_CENTER:
return LOG_STR("BASELINE_CENTER");
case TextAlign::BASELINE_RIGHT:
return LOG_STR("BASELINE_RIGHT");
case TextAlign::BOTTOM_LEFT:
return LOG_STR("BOTTOM_LEFT");
case TextAlign::BOTTOM_CENTER:
return LOG_STR("BOTTOM_CENTER");
case TextAlign::BOTTOM_RIGHT:
return LOG_STR("BOTTOM_RIGHT");
default:
return LOG_STR("UNKNOWN");
}
}
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,765 @@
#pragma once
#include <cstdarg>
#include <vector>
#include "rect.h"
#include "esphome/core/color.h"
#include "esphome/core/automation.h"
#include "esphome/core/time.h"
#include "esphome/core/log.h"
#include "display_color_utils.h"
#ifdef USE_GRAPH
#include "esphome/components/graph/graph.h"
#endif
#ifdef USE_QR_CODE
#include "esphome/components/qr_code/qr_code.h"
#endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
#include "esphome/components/graphical_display_menu/graphical_display_menu.h"
#endif
namespace esphome {
namespace display {
/** TextAlign is used to tell the display class how to position a piece of text. By default
* the coordinates you enter for the print*() functions take the upper left corner of the text
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
* refer to the *center* of the text.
*
* All text alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
* these options are allowed:
*
* - LEFT (x-coordinate of anchor point is on left)
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the text)
* - RIGHT (x-coordinate of anchor point is on right)
*
* For the Y-Axis alignment these options are allowed:
*
* - TOP (y-coordinate of anchor is on the top of the text)
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the text)
* - BASELINE (y-coordinate of anchor is on the baseline of the text)
* - BOTTOM (y-coordinate of anchor is on the bottom of the text)
*
* These options are then combined to create combined TextAlignment options like:
* - TOP_LEFT (default)
* - CENTER (anchor point is in the middle of the text bounds)
* - ...
*/
enum class TextAlign {
TOP = 0x00,
CENTER_VERTICAL = 0x01,
BASELINE = 0x02,
BOTTOM = 0x04,
LEFT = 0x00,
CENTER_HORIZONTAL = 0x08,
RIGHT = 0x10,
TOP_LEFT = TOP | LEFT,
TOP_CENTER = TOP | CENTER_HORIZONTAL,
TOP_RIGHT = TOP | RIGHT,
CENTER_LEFT = CENTER_VERTICAL | LEFT,
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
BASELINE_LEFT = BASELINE | LEFT,
BASELINE_CENTER = BASELINE | CENTER_HORIZONTAL,
BASELINE_RIGHT = BASELINE | RIGHT,
BOTTOM_LEFT = BOTTOM | LEFT,
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
BOTTOM_RIGHT = BOTTOM | RIGHT,
};
/** ImageAlign is used to tell the display class how to position a image. By default
* the coordinates you enter for the image() functions take the upper left corner of the image
* as the "anchor" point. You can customize this behavior to, for example, make the coordinates
* refer to the *center* of the image.
*
* All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis
* these options are allowed:
*
* - LEFT (x-coordinate of anchor point is on left)
* - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image)
* - RIGHT (x-coordinate of anchor point is on right)
*
* For the Y-Axis alignment these options are allowed:
*
* - TOP (y-coordinate of anchor is on the top of the image)
* - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image)
* - BOTTOM (y-coordinate of anchor is on the bottom of the image)
*
* These options are then combined to create combined TextAlignment options like:
* - TOP_LEFT (default)
* - CENTER (anchor point is in the middle of the image bounds)
* - ...
*/
enum class ImageAlign {
TOP = 0x00,
CENTER_VERTICAL = 0x01,
BOTTOM = 0x02,
LEFT = 0x00,
CENTER_HORIZONTAL = 0x04,
RIGHT = 0x08,
TOP_LEFT = TOP | LEFT,
TOP_CENTER = TOP | CENTER_HORIZONTAL,
TOP_RIGHT = TOP | RIGHT,
CENTER_LEFT = CENTER_VERTICAL | LEFT,
CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL,
CENTER_RIGHT = CENTER_VERTICAL | RIGHT,
BOTTOM_LEFT = BOTTOM | LEFT,
BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL,
BOTTOM_RIGHT = BOTTOM | RIGHT,
HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT,
VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM
};
enum DisplayType {
DISPLAY_TYPE_BINARY = 1,
DISPLAY_TYPE_GRAYSCALE = 2,
DISPLAY_TYPE_COLOR = 3,
};
enum DisplayRotation {
DISPLAY_ROTATION_0_DEGREES = 0,
DISPLAY_ROTATION_90_DEGREES = 90,
DISPLAY_ROTATION_180_DEGREES = 180,
DISPLAY_ROTATION_270_DEGREES = 270,
};
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
const int EDGES_QUADRILATERAL = 4;
const int EDGES_PENTAGON = 5;
const int EDGES_HEXAGON = 6;
const int EDGES_HEPTAGON = 7;
const int EDGES_OCTAGON = 8;
const int EDGES_NONAGON = 9;
const int EDGES_ENNEAGON = 9;
const int EDGES_DECAGON = 10;
const int EDGES_HENDECAGON = 11;
const int EDGES_DODECAGON = 12;
const int EDGES_TRIDECAGON = 13;
const int EDGES_TETRADECAGON = 14;
const int EDGES_PENTADECAGON = 15;
const int EDGES_HEXADECAGON = 16;
const float ROTATION_0_DEGREES = 0.0;
const float ROTATION_45_DEGREES = 45.0;
const float ROTATION_90_DEGREES = 90.0;
const float ROTATION_180_DEGREES = 180.0;
const float ROTATION_270_DEGREES = 270.0;
enum RegularPolygonVariation {
VARIATION_POINTY_TOP = 0,
VARIATION_FLAT_TOP = 1,
};
enum RegularPolygonDrawing {
DRAWING_OUTLINE = 0,
DRAWING_FILLED = 1,
};
class Display;
class DisplayPage;
class DisplayOnPageChangeTrigger;
using display_writer_t = std::function<void(Display &)>;
#define LOG_DISPLAY(prefix, type, obj) \
if ((obj) != nullptr) { \
ESP_LOGCONFIG(TAG, \
prefix type "\n" \
"%s Rotations: %d °\n" \
"%s Dimensions: %dpx x %dpx", \
prefix, (obj)->rotation_, prefix, (obj)->get_width(), (obj)->get_height()); \
}
/// Turn the pixel OFF.
extern const Color COLOR_OFF;
/// Turn the pixel ON.
extern const Color COLOR_ON;
class BaseImage {
public:
virtual void draw(int x, int y, Display *display, Color color_on, Color color_off) = 0;
virtual int get_width() const = 0;
virtual int get_height() const = 0;
};
class BaseFont {
public:
virtual void print(int x, int y, Display *display, Color color, const char *text, Color background) = 0;
virtual void measure(const char *str, int *width, int *x_offset, int *baseline, int *height) = 0;
};
class Display : public PollingComponent {
public:
/// Fill the entire screen with the given color.
virtual void fill(Color color);
/// Clear the entire screen by filling it with OFF pixels.
void clear();
/// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); }
/// Get the calculated height of the display in pixels with rotation applied.
virtual int get_height() { return this->get_height_internal(); }
/// Get the native (original) width of the display in pixels.
int get_native_width() { return this->get_width_internal(); }
/// Get the native (original) height of the display in pixels.
int get_native_height() { return this->get_height_internal(); }
/// Set a single pixel at the specified coordinates to default color.
inline void draw_pixel_at(int x, int y) { this->draw_pixel_at(x, y, COLOR_ON); }
/// Set a single pixel at the specified coordinates to the given color.
virtual void draw_pixel_at(int x, int y, Color color) = 0;
/** Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
* The naive implementation here will work in all cases, but can be overridden by sub-classes
* in order to optimise the procedure.
* The parameters describe a rectangular block of pixels, potentially within a larger buffer.
*
* \param x_start The starting destination x position
* \param y_start The starting destination y position
* \param w the width of the pixel block
* \param h the height of the pixel block
* \param ptr A pointer to the start of the data to be copied
* \param order The ordering of the colors
* \param bitness Defines the number of bits and their format for each pixel
* \param big_endian True if 16 bit values are stored big-endian
* \param x_offset The initial x-offset into the source buffer.
* \param y_offset The initial y-offset into the source buffer.
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
*
* The length of each source buffer line (stride) will be x_offset + w + x_pad.
*/
virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
/// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.)
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian) {
this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0);
}
/// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
/// Draw a straight line at the given angle based on the origin [x, y] for a specified length with the given color.
void line_at_angle(int x, int y, int angle, int length, Color color = COLOR_ON);
/// Draw a straight line at the given angle based on the origin [x, y] from a specified start and stop radius with the
/// given color.
void line_at_angle(int x, int y, int angle, int start_radius, int stop_radius, Color color = COLOR_ON);
/// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color.
void horizontal_line(int x, int y, int width, Color color = COLOR_ON);
/// Draw a vertical line from the point [x,y] to [x,y+width] with the given color.
void vertical_line(int x, int y, int height, Color color = COLOR_ON);
/// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at
/// [x1+width,y1+height].
void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
/// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height].
void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON);
/// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color.
void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON);
/// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
/// Fill a ring centered around [center_x,center_y] between two circles with the radius1 and radius2 with the given
/// color.
void filled_ring(int center_x, int center_y, int radius1, int radius2, Color color = COLOR_ON);
/// Fill a half-ring "gauge" centered around [center_x,center_y] between two circles with the radius1 and radius2
/// with he given color and filled up to 'progress' percent
void filled_gauge(int center_x, int center_y, int radius1, int radius2, int progress, Color color = COLOR_ON);
/// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
/// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
/// Get the specified vertex (x,y) coordinates for the regular polygon inscribed in the circle centered on
/// [center_x,center_y] with the given radius. Vertex id are 0-indexed and rotate clockwise. In a pointy-topped
/// variation of a polygon with a 0° rotation, the vertex #0 is located at the top of the polygon. In a flat-topped
/// variation of a polygon with a 0° rotation, the vertex #0 is located on the left-side of the horizontal top
/// edge, and the vertex #1 is located on the right-side of the horizontal top edge.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
void get_regular_polygon_vertex(int vertex_id, int *vertex_x, int *vertex_y, int center_x, int center_y, int radius,
int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES);
/// Draw the outline of a regular polygon inscribed in the circle centered on [x,y] with the given
/// radius and color.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
/// Use the drawing to switch between outlining or filling the polygon.
void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
void regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
void regular_polygon(int x, int y, int radius, int edges, Color color,
RegularPolygonDrawing drawing = DRAWING_OUTLINE);
/// Fill a regular polygon inscribed in the circle centered on [x,y] with the given radius and color.
/// Use the edges constants (e.g.: EDGES_HEXAGON) or any integer to specify the number of edges of the polygon.
/// Use the variation to switch between the flat-topped or the pointy-topped variation of the polygon.
/// Use the rotation in degrees to rotate the shape clockwise.
void filled_regular_polygon(int x, int y, int radius, int edges,
RegularPolygonVariation variation = VARIATION_POINTY_TOP,
float rotation_degrees = ROTATION_0_DEGREES, Color color = COLOR_ON);
void filled_regular_polygon(int x, int y, int radius, int edges, RegularPolygonVariation variation, Color color);
void filled_regular_polygon(int x, int y, int radius, int edges, Color color);
/** Print `text` with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param align The alignment of the text.
* @param text The text to draw.
* @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels
*/
void print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text,
Color background = COLOR_OFF);
/** Print `text` with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param text The text to draw.
* @param background When using multi-bit (anti-aliased) fonts, blend this background color into pixels
*/
void print(int x, int y, BaseFont *font, Color color, const char *text, Color background = COLOR_OFF);
/** Print `text` with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param align The alignment of the text.
* @param text The text to draw.
*/
void print(int x, int y, BaseFont *font, TextAlign align, const char *text);
/** Print `text` with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param text The text to draw.
*/
void print(int x, int y, BaseFont *font, const char *text);
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param background The background color to use for anti-aliasing
* @param align The alignment of the text.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format, ...)
__attribute__((format(printf, 8, 9)));
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param align The alignment of the text.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ...)
__attribute__((format(printf, 7, 8)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, BaseFont *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param align The alignment of the text.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, BaseFont *font, TextAlign align, const char *format, ...)
__attribute__((format(printf, 6, 7)));
/** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void printf(int x, int y, BaseFont *font, const char *format, ...) __attribute__((format(printf, 5, 6)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param background The background color to draw the text with.
* @param align The alignment of the text.
* @param format The format to use.
* @param ... The arguments to use for the text formatting.
*/
void strftime(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
ESPTime time) __attribute__((format(strftime, 8, 0)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param align The alignment of the text.
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, BaseFont *font, Color color, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 7, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param color The color to draw the text with.
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, BaseFont *font, Color color, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`.
*
* @param x The x coordinate of the text alignment anchor point.
* @param y The y coordinate of the text alignment anchor point.
* @param font The font to draw the text with.
* @param align The alignment of the text.
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, BaseFont *font, TextAlign align, const char *format, ESPTime time)
__attribute__((format(strftime, 6, 0)));
/** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param font The font to draw the text with.
* @param format The strftime format to use.
* @param time The time to format.
*/
void strftime(int x, int y, BaseFont *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0)));
/** Draw the `image` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param image The image to draw.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
/** Draw the `image` at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param image The image to draw.
* @param align The alignment of the image.
* @param color_on The color to replace in binary images for the on bits.
* @param color_off The color to replace in binary images for the off bits.
*/
void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
#ifdef USE_GRAPH
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param graph The graph id to draw
* @param color_on The color to replace in binary images for the on bits.
*/
void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
/** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param graph The graph id for which the legend applies to
* @param graph The graph id for which the legend applies to
* @param graph The graph id for which the legend applies to
* @param name_font The font used for the trace name
* @param value_font The font used for the trace value and units
* @param color_on The color of the border
*/
void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
#endif // USE_GRAPH
#ifdef USE_QR_CODE
/** Draw the `qr_code` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param qr_code The qr_code to draw
* @param color_on The color to replace in binary images for the on bits.
*/
void qr_code(int x, int y, qr_code::QrCode *qr_code, Color color_on = COLOR_ON, int scale = 1);
#endif
#ifdef USE_GRAPHICAL_DISPLAY_MENU
/**
* @param x The x coordinate of the upper left corner
* @param y The y coordinate of the upper left corner
* @param menu The GraphicalDisplayMenu to draw
* @param width Width of the menu
* @param height Height of the menu
*/
void menu(int x, int y, graphical_display_menu::GraphicalDisplayMenu *menu, int width, int height);
#endif // USE_GRAPHICAL_DISPLAY_MENU
/** Get the text bounds of the given string.
*
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
* @param y The y coordinate to place the string at, can be 0 if only interested in dimensions.
* @param text The text to measure.
* @param font The font to measure the text bounds with.
* @param align The alignment of the text. Set to TextAlign::TOP_LEFT if only interested in dimensions.
* @param x1 A pointer to store the returned x coordinate of the upper left corner in.
* @param y1 A pointer to store the returned y coordinate of the upper left corner in.
* @param width A pointer to store the returned text width in.
* @param height A pointer to store the returned text height in.
*/
void get_text_bounds(int x, int y, const char *text, BaseFont *font, TextAlign align, int *x1, int *y1, int *width,
int *height);
/// Internal method to set the display writer lambda.
void set_writer(display_writer_t &&writer);
void show_page(DisplayPage *page);
void show_next_page();
void show_prev_page();
void set_pages(std::vector<DisplayPage *> pages);
const DisplayPage *get_active_page() const { return this->page_; }
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
/// Internal method to set the display rotation with.
void set_rotation(DisplayRotation rotation);
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
DisplayRotation get_rotation() const { return this->rotation_; }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
virtual DisplayType get_display_type() = 0;
/** Set the clipping rectangle for further drawing
*
* @param[in] rect: Pointer to Rect for clipping (or NULL for entire screen)
*
* return true if success, false if error
*/
void start_clipping(Rect rect);
void start_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
start_clipping(Rect(left, top, right - left, bottom - top));
};
/** Add a rectangular region to the invalidation region
* - This is usually called when an element has been modified
*
* @param[in] rect: Rectangle to add to the invalidation region
*/
void extend_clipping(Rect rect);
void extend_clipping(int16_t left, int16_t top, int16_t right, int16_t bottom) {
this->extend_clipping(Rect(left, top, right - left, bottom - top));
};
/** substract a rectangular region to the invalidation region
* - This is usually called when an element has been modified
*
* @param[in] rect: Rectangle to add to the invalidation region
*/
void shrink_clipping(Rect rect);
void shrink_clipping(uint16_t left, uint16_t top, uint16_t right, uint16_t bottom) {
this->shrink_clipping(Rect(left, top, right - left, bottom - top));
};
/** Reset the invalidation region
*/
void end_clipping();
/** Get the current the clipping rectangle
*
* return rect for active clipping region
*/
Rect get_clipping() const;
bool is_clipping() const { return !this->clipping_rectangle_.empty(); }
/** Check if pixel is within region of display.
*/
bool clip(int x, int y);
void test_card();
void show_test_card() { this->show_test_card_ = true; }
protected:
bool clamp_x_(int x, int w, int &min_x, int &max_x);
bool clamp_y_(int y, int h, int &min_y, int &max_y);
void vprintf_(int x, int y, BaseFont *font, Color color, Color background, TextAlign align, const char *format,
va_list arg);
void do_update_();
void clear_clipping_();
virtual int get_height_internal() = 0;
virtual int get_width_internal() = 0;
/**
* This method fills a triangle using only integer variables by using a
* modified bresenham algorithm.
* It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line,
* so y2 must be equal to y3.
*/
void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color);
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
optional<display_writer_t> writer_{};
DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
bool auto_clear_enabled_{true};
std::vector<Rect> clipping_rectangle_;
bool show_test_card_{false};
};
class DisplayPage {
public:
DisplayPage(display_writer_t writer);
void show();
void show_next();
void show_prev();
void set_parent(Display *parent);
void set_prev(DisplayPage *prev);
void set_next(DisplayPage *next);
const display_writer_t &get_writer() const;
protected:
Display *parent_;
display_writer_t writer_;
DisplayPage *prev_{nullptr};
DisplayPage *next_{nullptr};
};
template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public:
TEMPLATABLE_VALUE(DisplayPage *, page)
void play(Ts... x) override {
auto *page = this->page_.value(x...);
if (page != nullptr) {
page->show();
}
}
};
template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...> {
public:
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
void play(Ts... x) override { this->buffer_->show_next_page(); }
Display *buffer_;
};
template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...> {
public:
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
void play(Ts... x) override { this->buffer_->show_prev_page(); }
Display *buffer_;
};
template<typename... Ts> class DisplayIsDisplayingPageCondition : public Condition<Ts...> {
public:
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
void set_page(DisplayPage *page) { this->page_ = page; }
bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
protected:
Display *parent_;
DisplayPage *page_;
};
class DisplayOnPageChangeTrigger : public Trigger<DisplayPage *, DisplayPage *> {
public:
explicit DisplayOnPageChangeTrigger(Display *parent) { parent->add_on_page_change_trigger(this); }
void process(DisplayPage *from, DisplayPage *to);
void set_from(DisplayPage *p) { this->from_ = p; }
void set_to(DisplayPage *p) { this->to_ = p; }
protected:
DisplayPage *from_{nullptr};
DisplayPage *to_{nullptr};
};
const LogString *text_align_to_string(TextAlign textalign);
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,72 @@
#include "display_buffer.h"
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
RAMAllocator<uint8_t> allocator;
this->buffer_ = allocator.allocate(buffer_length);
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for display!");
return;
}
this->clear();
}
int DisplayBuffer::get_width() {
switch (this->rotation_) {
case DISPLAY_ROTATION_90_DEGREES:
case DISPLAY_ROTATION_270_DEGREES:
return this->get_height_internal();
case DISPLAY_ROTATION_0_DEGREES:
case DISPLAY_ROTATION_180_DEGREES:
default:
return this->get_width_internal();
}
}
int DisplayBuffer::get_height() {
switch (this->rotation_) {
case DISPLAY_ROTATION_0_DEGREES:
case DISPLAY_ROTATION_180_DEGREES:
return this->get_height_internal();
case DISPLAY_ROTATION_90_DEGREES:
case DISPLAY_ROTATION_270_DEGREES:
default:
return this->get_width_internal();
}
}
void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return; // NOLINT
switch (this->rotation_) {
case DISPLAY_ROTATION_0_DEGREES:
break;
case DISPLAY_ROTATION_90_DEGREES:
std::swap(x, y);
x = this->get_width_internal() - x - 1;
break;
case DISPLAY_ROTATION_180_DEGREES:
x = this->get_width_internal() - x - 1;
y = this->get_height_internal() - y - 1;
break;
case DISPLAY_ROTATION_270_DEGREES:
std::swap(x, y);
y = this->get_height_internal() - y - 1;
break;
}
this->draw_absolute_pixel_internal(x, y, color);
App.feed_wdt();
}
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,34 @@
#pragma once
#include <cstdarg>
#include <vector>
#include "display.h"
#include "display_color_utils.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
namespace esphome {
namespace display {
class DisplayBuffer : public Display {
public:
/// Get the width of the image in pixels with rotation applied.
int get_width() override;
/// Get the height of the image in pixels with rotation applied.
int get_height() override;
/// Set a single pixel at the specified coordinates to the given color.
void draw_pixel_at(int x, int y, Color color) override;
protected:
virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0;
void init_internal_(uint32_t buffer_length);
uint8_t *buffer_{nullptr};
};
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,159 @@
#pragma once
#include "esphome/core/color.h"
namespace esphome {
namespace display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
enum ColorBitness : uint8_t { COLOR_BITNESS_888 = 0, COLOR_BITNESS_565 = 1, COLOR_BITNESS_332 = 2 };
inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 255) { return (max_value * i / scale); }
class ColorUtil {
public:
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
uint8_t first_bits = 0;
uint8_t second_bits = 0;
uint8_t third_bits = 0;
switch (color_bitness) {
case COLOR_BITNESS_888:
first_bits = 8;
second_bits = 8;
third_bits = 8;
break;
case COLOR_BITNESS_565:
first_bits = 5;
second_bits = 6;
third_bits = 5;
break;
case COLOR_BITNESS_332:
first_bits = 3;
second_bits = 3;
third_bits = 2;
break;
}
first_color = right_bit_aligned ? esp_scale(((colorcode >> (second_bits + third_bits)) & ((1 << first_bits) - 1)),
((1 << first_bits) - 1))
: esp_scale(((colorcode >> 16) & 0xFF), (1 << first_bits) - 1);
second_color = right_bit_aligned
? esp_scale(((colorcode >> third_bits) & ((1 << second_bits) - 1)), ((1 << second_bits) - 1))
: esp_scale(((colorcode >> 8) & 0xFF), ((1 << second_bits) - 1));
third_color = (right_bit_aligned ? esp_scale(((colorcode >> 0) & ((1 << third_bits) - 1)), ((1 << third_bits) - 1))
: esp_scale(((colorcode >> 0) & 0xFF), (1 << third_bits) - 1));
Color color_return;
switch (color_order) {
case COLOR_ORDER_RGB:
color_return.r = first_color;
color_return.g = second_color;
color_return.b = third_color;
break;
case COLOR_ORDER_BGR:
color_return.b = first_color;
color_return.g = second_color;
color_return.r = third_color;
break;
case COLOR_ORDER_GRB:
color_return.g = first_color;
color_return.r = second_color;
color_return.b = third_color;
break;
}
return color_return;
}
static inline Color rgb332_to_color(uint8_t rgb332_color) {
return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332);
}
static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
uint16_t red_color, green_color, blue_color;
red_color = esp_scale8(color.red, ((1 << 3) - 1));
green_color = esp_scale8(color.green, ((1 << 3) - 1));
blue_color = esp_scale8(color.blue, (1 << 2) - 1);
switch (color_order) {
case COLOR_ORDER_RGB:
return red_color << 5 | green_color << 2 | blue_color;
case COLOR_ORDER_BGR:
return blue_color << 6 | green_color << 3 | red_color;
case COLOR_ORDER_GRB:
return green_color << 5 | red_color << 2 | blue_color;
}
return 0;
}
static uint16_t color_to_565(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) {
uint16_t red_color, green_color, blue_color;
red_color = esp_scale8(color.red, ((1 << 5) - 1));
green_color = esp_scale8(color.green, ((1 << 6) - 1));
blue_color = esp_scale8(color.blue, (1 << 5) - 1);
switch (color_order) {
case COLOR_ORDER_RGB:
return red_color << 11 | green_color << 5 | blue_color;
case COLOR_ORDER_BGR:
return blue_color << 11 | green_color << 5 | red_color;
case COLOR_ORDER_GRB:
return green_color << 10 | red_color << 5 | blue_color;
}
return 0;
}
static uint32_t color_to_grayscale4(Color color) {
uint32_t gs4 = esp_scale8(color.white, 15);
return gs4;
}
/***
* Converts a Color value to an 8bit index using a 24bit 888 palette.
* Uses euclidiean distance to calculate the linear distance between
* two points in an RGB cube, then iterates through the full palette
* returning the closest match.
* @param[in] color The target color.
* @param[in] palette The 256*3 byte RGB palette.
* @return The 8 bit index of the closest color (e.g. for display buffer).
*/
// static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) {
static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) {
uint8_t closest_index = 0;
uint32_t minimum_dist2 = UINT32_MAX; // Smallest distance^2 to the target
// so far
// int8_t(*plt)[][3] = palette;
int16_t tgt_r = color.r;
int16_t tgt_g = color.g;
int16_t tgt_b = color.b;
uint16_t x, y, z;
// Loop through each row of the palette
for (uint16_t i = 0; i < 256; i++) {
// Get the pallet rgb color
int16_t plt_r = (int16_t) palette[i * 3 + 0];
int16_t plt_g = (int16_t) palette[i * 3 + 1];
int16_t plt_b = (int16_t) palette[i * 3 + 2];
// Calculate euclidean distance (linear distance in rgb cube).
x = (uint32_t) std::abs(tgt_r - plt_r);
y = (uint32_t) std::abs(tgt_g - plt_g);
z = (uint32_t) std::abs(tgt_b - plt_b);
uint32_t dist2 = x * x + y * y + z * z;
if (dist2 < minimum_dist2) {
minimum_dist2 = dist2;
closest_index = (uint8_t) i;
}
}
return closest_index;
}
/***
* Converts an 8bit palette index (e.g. from a display buffer) to a color.
* @param[in] index The index to look up.
* @param[in] palette The 256*3 byte RGB palette.
* @return The RGBW Color object looked up by the palette.
*/
static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) {
Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0);
return color;
}
};
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,94 @@
#include "rect.h"
#include "esphome/core/log.h"
namespace esphome {
namespace display {
static const char *const TAG = "display";
void Rect::expand(int16_t horizontal, int16_t vertical) {
if (this->is_set() && (this->w >= (-2 * horizontal)) && (this->h >= (-2 * vertical))) {
this->x = this->x - horizontal;
this->y = this->y - vertical;
this->w = this->w + (2 * horizontal);
this->h = this->h + (2 * vertical);
}
}
void Rect::extend(Rect rect) {
if (!this->is_set()) {
this->x = rect.x;
this->y = rect.y;
this->w = rect.w;
this->h = rect.h;
} else {
if (this->x > rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y > rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
if (this->x2() < rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->y2() < rect.y2()) {
this->h = rect.y2() - this->y;
}
}
}
void Rect::shrink(Rect rect) {
if (!this->inside(rect)) {
(*this) = Rect();
} else {
if (this->x2() > rect.x2()) {
this->w = rect.x2() - this->x;
}
if (this->x < rect.x) {
this->w = this->w + (this->x - rect.x);
this->x = rect.x;
}
if (this->y2() > rect.y2()) {
this->h = rect.y2() - this->y;
}
if (this->y < rect.y) {
this->h = this->h + (this->y - rect.y);
this->y = rect.y;
}
}
}
bool Rect::equal(Rect rect) const {
return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h);
}
bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT
if (!this->is_set()) {
return true;
}
if (absolute) {
return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2();
}
return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h;
}
bool Rect::inside(Rect rect) const {
if (!this->is_set() || !rect.is_set()) {
return true;
}
return this->x2() >= rect.x && this->x <= rect.x2() && this->y2() >= rect.y && this->y <= rect.y2();
}
void Rect::info(const std::string &prefix) {
if (this->is_set()) {
ESP_LOGI(TAG, "%s [%3d,%3d,%3d,%3d] (%3d,%3d)", prefix.c_str(), this->x, this->y, this->w, this->h, this->x2(),
this->y2());
} else {
ESP_LOGI(TAG, "%s ** IS NOT SET **", prefix.c_str());
}
}
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,36 @@
#pragma once
#include "esphome/core/helpers.h"
namespace esphome {
namespace display {
static const int16_t VALUE_NO_SET = 32766;
class Rect {
public:
int16_t x; ///< X coordinate of corner
int16_t y; ///< Y coordinate of corner
int16_t w; ///< Width of region
int16_t h; ///< Height of region
Rect() : x(VALUE_NO_SET), y(VALUE_NO_SET), w(VALUE_NO_SET), h(VALUE_NO_SET) {} // NOLINT
inline Rect(int16_t x, int16_t y, int16_t w, int16_t h) ESPHOME_ALWAYS_INLINE : x(x), y(y), w(w), h(h) {}
inline int16_t x2() const { return this->x + this->w; }; ///< X coordinate of corner
inline int16_t y2() const { return this->y + this->h; }; ///< Y coordinate of corner
inline bool is_set() const ESPHOME_ALWAYS_INLINE { return (this->h != VALUE_NO_SET) && (this->w != VALUE_NO_SET); }
void expand(int16_t horizontal, int16_t vertical);
void extend(Rect rect);
void shrink(Rect rect);
bool inside(Rect rect) const;
bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const;
bool equal(Rect rect) const;
void info(const std::string &prefix = "rect info:");
};
} // namespace display
} // namespace esphome

View File

@@ -0,0 +1,7 @@
```yaml
# example configuration:
cover:
platform: stepper_cover
name: stepper cover
```

View File

@@ -0,0 +1,17 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import cover
from esphome.const import CONF_ID
stepper_cover_ns = cg.esphome_ns.namespace("stepper_cover")
StepperCover = stepper_cover_ns.class_("StepperCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(StepperCover)}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await cover.register_cover(var, config)

View File

@@ -0,0 +1,35 @@
#include "esphome/core/log.h"
#include "stepper_cover.h"
namespace esphome {
namespace stepper_cover {
static const char *TAG = "stepper_cover.cover";
void StepperCover::setup() {
}
void StepperCover::loop() {
}
void StepperCover::dump_config() {
ESP_LOGCONFIG(TAG, "Empty cover");
}
cover::CoverTraits StepperCover::get_traits() {
auto traits = cover::CoverTraits();
traits.set_is_assumed_state(false);
traits.set_supports_position(false);
traits.set_supports_tilt(false);
return traits;
}
void StepperCover::control(const cover::CoverCall &call) {
}
} // namespace stepper_cover
} // namespace esphome

View File

@@ -0,0 +1,21 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace stepper_cover {
class StepperCover : public cover::Cover, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
cover::CoverTraits get_traits() override;
protected:
void control(const cover::CoverCall &call) override;
};
} // namespace stepper_cover
} // namespace esphome

View File

@@ -0,0 +1,3 @@
{
"cmake.ignoreCMakeListsMissing": true
}

View File

@@ -0,0 +1,5 @@
idf_component_register(
SRC_DIRS "."
INCLUDE_DIRS "."
REQUIRES "spi"
)

View File

@@ -0,0 +1,91 @@
EXAMPLE_USAGE.md
# ESPHome Nixie Tube Display Component
This is a custom ESPHome component for controlling a 6-digit nixie tube display with SPI interface.
## Installation
1. Copy the `esphome_component` folder to your ESPHome custom components directory:
```
~/.esphome/custom_components/nixie_display/
```
2. The component should have:
- `__init__.py` - Component configuration
- `nixie_display.h` - Header file
- `nixie_display.cpp` - Implementation
## Configuration
Add to your ESPHome YAML:
```yaml
spi:
id: nixie_spi
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
display:
- platform: nixie_display
id: nixie
spi_id: nixie_spi
anode0_pin: GPIO5
anode1_pin: GPIO13
anode2_pin: GPIO17
le_pin: GPIO22
# Lambda to update display with time
lambda: |-
it.display_text("000000"); // Display as string of 6 digits
text_sensor:
- platform: homeassistant
id: time_display
entity_id: sensor.time
```
## Features
- **6 Nixie Tubes**: Organized as 3 anode sets of 2 tubes each
- **SPI Control**: Fast serial interface for cathode control
- **Anti-poisoning**: Implements digit cycling to prevent cathode poisoning
- **Text Display**: Can display any 6-character string of digits
- **60 Hz Refresh Rate**: Smooth flicker-free display
## Pin Configuration
- `anode0_pin`: Controls first tube pair (digits 0-1)
- `anode1_pin`: Controls second tube pair (digits 2-3)
- `anode2_pin`: Controls third tube pair (digits 4-5)
- `le_pin`: Latch Enable pin for SPI data locking
- SPI pins: CLK, MOSI, MISO (configured via SPI component)
## Display Format
The display expects a 6-character string of digits (0-9):
- Position 0-1: First anode set
- Position 2-3: Second anode set
- Position 4-5: Third anode set
## Usage Example
```yaml
lambda: |-
// Display current time
auto time_obj = id(homeassistant_time).now();
if (time_obj.is_valid()) {
char buf[7];
snprintf(buf, sizeof(buf), "%02d%02d%02d",
time_obj.hour, time_obj.minute, time_obj.second);
it.display_text(buf);
}
```
## Anti-Poisoning Feature
Automatically enabled during transitions to cycle through digit values and prevent cathode poisoning that occurs with static displays. The algorithm:
1. Cycles all digits for 10 iterations
2. Then incrementally changes digits to target values
3. Total cycle takes ~20 iterations at the refresh rate

View File

@@ -0,0 +1,2 @@
CODEOWNERS = ["@yourgithubname"]
DEPENDENCIES = ["spi"]

View File

@@ -0,0 +1,63 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import display, spi, time
from esphome.const import CONF_ID, CONF_SPI_ID
DEPENDENCIES = ["spi", "time"]
nixie_display_ns = cg.esphome_ns.namespace("nixie_display")
NixieDisplay = nixie_display_ns.class_(
"NixieDisplay", display.DisplayBuffer, spi.SPIDevice
)
CONF_ANODE0_PIN = "anode0_pin"
CONF_ANODE1_PIN = "anode1_pin"
CONF_ANODE2_PIN = "anode2_pin"
CONF_LE_PIN = "le_pin"
CONF_REFRESH_INTERVAL_US = "refresh_interval_us"
CONF_TIME_ID = "time_id"
CONF_DEBUG_LOGGING = "debug_logging"
CONF_ANTI_POISONING = "anti_poisoning"
CONF_LAMBDA_HOLD_MS = "lambda_hold_ms"
CONF_AUTO_TIME = "auto_time"
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(NixieDisplay),
cv.Required(CONF_SPI_ID): cv.use_id(spi.SPIComponent),
cv.Required(CONF_ANODE0_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_ANODE1_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_ANODE2_PIN): pins.gpio_output_pin_schema,
cv.Required(CONF_LE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_REFRESH_INTERVAL_US, default=1000): cv.int_range(
min=200, max=20000
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(CONF_DEBUG_LOGGING, default=False): cv.boolean,
cv.Optional(CONF_ANTI_POISONING, default=True): cv.boolean,
cv.Optional(CONF_LAMBDA_HOLD_MS, default=1500): cv.int_range(min=0, max=60000),
cv.Optional(CONF_AUTO_TIME, default=True): cv.boolean,
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await spi.register_spi_device(var, config)
anode0 = await cg.gpio_pin_expression(config[CONF_ANODE0_PIN])
anode1 = await cg.gpio_pin_expression(config[CONF_ANODE1_PIN])
anode2 = await cg.gpio_pin_expression(config[CONF_ANODE2_PIN])
le_pin = await cg.gpio_pin_expression(config[CONF_LE_PIN])
cg.add(var.set_anode_pins(anode0, anode1, anode2, le_pin))
cg.add(var.set_refresh_interval_us(config[CONF_REFRESH_INTERVAL_US]))
cg.add(var.set_debug_logging(config[CONF_DEBUG_LOGGING]))
cg.add(var.set_anti_poisoning(config[CONF_ANTI_POISONING]))
cg.add(var.set_lambda_hold_ms(config[CONF_LAMBDA_HOLD_MS]))
cg.add(var.set_auto_time(config[CONF_AUTO_TIME]))
if CONF_TIME_ID in config:
time_var = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time_source(time_var))

View File

@@ -0,0 +1,111 @@
esphome:
name: nixie-clock
platform: esp32
board: esp32-c3-devkitm-1
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Nixie-Clock"
password: "12345678"
captive_portal:
logger:
level: INFO
api:
encryption:
key: !secret api_encryption_key
ota:
password: !secret ota_password
time:
- platform: sntp
id: sntp_time
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
spi:
id: nixie_spi
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
frequency: 1MHz
display:
- platform: nixie_display
id: nixie_clock
spi_id: nixie_spi
anode0_pin: GPIO5
anode1_pin: GPIO13
anode2_pin: GPIO17
le_pin: GPIO22
# Update display with current time HH:MM:SS
lambda: |-
auto time_obj = id(sntp_time).now();
if (time_obj.is_valid()) {
char time_str[7];
snprintf(time_str, sizeof(time_str), "%02d%02d%02d",
time_obj.hour,
time_obj.minute,
time_obj.second);
it.printf(time_str);
} else {
it.printf("000000");
}
return;
# Buttons from defines.h
binary_sensor:
- platform: gpio
pin:
number: GPIO2
mode: INPUT_PULLUP
name: "Up Button"
on_press:
then:
- logger.log: "Up button pressed"
- platform: gpio
pin:
number: GPIO4
mode: INPUT_PULLUP
name: "Down Button"
on_press:
then:
- logger.log: "Down button pressed"
- platform: gpio
pin:
number: GPIO16
mode: INPUT_PULLUP
name: "Mode Button"
on_press:
then:
- logger.log: "Mode button pressed"
# Underglow LED (RGB) for tube illumination from defines.h
light:
- platform: rgb
name: "Tube Underglow"
red: underglow_led_r
green: underglow_led_g
blue: underglow_led_b
restore_mode: RESTORE_DEFAULT_ON
output:
- platform: gpio
pin: GPIO25
id: underglow_led_r
- platform: gpio
pin: GPIO26
id: underglow_led_g
- platform: gpio
pin: GPIO27
id: underglow_led_b

View File

@@ -0,0 +1,323 @@
#include "nixie_display.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <ctime>
#include <cstdio>
namespace esphome {
namespace nixie_display {
static const char *const TAG = "nixie_display";
void NixieDisplay::setup() {
ESP_LOGI(TAG, "Setting up Nixie Display (6 tubes)");
// Setup GPIO pins
this->anode0_pin_->setup();
this->anode1_pin_->setup();
this->anode2_pin_->setup();
this->le_pin_->setup();
// Initialize display
this->displayed_string_ = "120000";
this->target_string_ = this->displayed_string_;
this->current_mode_ = MODE_TIME;
this->target_mode_ = MODE_TIME;
this->current_anode_ = 0;
this->last_update_us_ = micros();
this->last_external_text_ms_ = millis();
this->anti_poison_active_ = false;
this->anti_poison_counter_ = 0;
// Initialize SPI through parent class
this->spi_setup();
// Request high-frequency main loop scheduling to keep multiplexing stable.
this->high_freq_.start();
}
void NixieDisplay::printf(const char *format, ...) {
char buffer[32];
va_list args;
va_start(args, format);
vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
this->set_target_string_(buffer, MODE_UNKNOWN);
}
void NixieDisplay::dump_config() {
ESP_LOGCONFIG(TAG, "Nixie Display:");
LOG_PIN(" Anode 0 Pin: ", this->anode0_pin_);
LOG_PIN(" Anode 1 Pin: ", this->anode1_pin_);
LOG_PIN(" Anode 2 Pin: ", this->anode2_pin_);
LOG_PIN(" LE Pin: ", this->le_pin_);
ESP_LOGCONFIG(TAG, " Refresh interval: %u us", this->refresh_interval_us_);
ESP_LOGCONFIG(TAG, " Time source configured: %s", this->time_source_ != nullptr ? "YES" : "NO");
ESP_LOGCONFIG(TAG, " Debug logging: %s", this->debug_logging_ ? "YES" : "NO");
ESP_LOGCONFIG(TAG, " Anti poisoning: %s", this->anti_poisoning_ ? "YES" : "NO");
ESP_LOGCONFIG(TAG, " Auto time: %s", this->auto_time_ ? "YES" : "NO");
ESP_LOGCONFIG(TAG, " Date mode: %s", this->date_mode_ ? "ON" : "OFF");
ESP_LOGCONFIG(TAG, " Auto rotate: %s", this->auto_rotate_ ? "ON" : "OFF");
ESP_LOGCONFIG(TAG, " Anti poison on mode change: %s", this->anti_poison_on_mode_change_ ? "YES" : "NO");
ESP_LOGCONFIG(TAG, " Lambda hold: %u ms", this->lambda_hold_ms_);
}
void NixieDisplay::update() {
// Keep default display integration behavior available.
this->do_update_();
}
void NixieDisplay::loop() {
const uint32_t now_ms = millis();
// Render desired content at a fixed logic cadence.
if ((now_ms - this->last_logic_update_ms_) >= 200) {
this->last_logic_update_ms_ = now_ms;
bool effective_date_mode = this->date_mode_;
if (this->auto_rotate_) {
const time_t rotate_epoch = ::time(nullptr);
if (rotate_epoch > 1700000000) {
struct tm rotate_tm;
localtime_r(&rotate_epoch, &rotate_tm);
// Show date for 5 seconds every 30 seconds when auto-rotate is enabled.
effective_date_mode = (rotate_tm.tm_sec % 30) < 5;
}
}
// Run YAML lambda/pages only when a writer exists.
if (this->writer_.has_value()) {
this->do_update_();
}
// Let lambda-provided content temporarily override auto-rendered content.
const bool lambda_override_active =
(this->lambda_hold_ms_ > 0) && ((now_ms - this->last_external_text_ms_) < this->lambda_hold_ms_);
if (this->auto_time_ && !lambda_override_active) {
bool wrote_time = false;
if (this->time_source_ != nullptr) {
auto now = this->time_source_->now();
if (now.is_valid()) {
const bool mode_changed = (effective_date_mode != this->last_effective_date_mode_);
const bool second_changed = (now.second != this->last_render_second_);
if (mode_changed || second_changed) {
char buf[7];
if (effective_date_mode) {
snprintf(buf, sizeof(buf), "%02d%02d%02d", now.day_of_month, now.month, now.year % 100);
this->set_target_string_(buf, MODE_DATE);
} else {
snprintf(buf, sizeof(buf), "%02d%02d%02d", now.hour, now.minute, now.second);
this->set_target_string_(buf, MODE_TIME);
}
this->last_render_second_ = now.second;
this->last_effective_date_mode_ = effective_date_mode;
}
wrote_time = true;
}
}
// Always allow system-time fallback even if time_id wiring failed.
if (!wrote_time) {
const time_t epoch = ::time(nullptr);
if (epoch > 1700000000) {
struct tm tm_now;
localtime_r(&epoch, &tm_now);
const bool mode_changed = (effective_date_mode != this->last_effective_date_mode_);
const bool second_changed = (tm_now.tm_sec != this->last_render_second_);
if (mode_changed || second_changed) {
char buf[7];
if (effective_date_mode) {
snprintf(buf, sizeof(buf), "%02d%02d%02d", tm_now.tm_mday, tm_now.tm_mon + 1,
(tm_now.tm_year + 1900) % 100);
this->set_target_string_(buf, MODE_DATE);
} else {
snprintf(buf, sizeof(buf), "%02d%02d%02d", tm_now.tm_hour, tm_now.tm_min,
tm_now.tm_sec);
this->set_target_string_(buf, MODE_TIME);
}
this->last_render_second_ = tm_now.tm_sec;
this->last_effective_date_mode_ = effective_date_mode;
}
} else {
this->set_target_string_(effective_date_mode ? "010100" : "120000",
effective_date_mode ? MODE_DATE : MODE_TIME);
this->last_render_second_ = -1;
this->last_effective_date_mode_ = effective_date_mode;
}
}
}
if (this->anti_poisoning_ && this->anti_poison_pending_ && this->displayed_string_ != this->target_string_) {
this->displayed_string_ = this->anti_poison_transition_(this->displayed_string_, this->target_string_);
// anti_poison_transition_ marks anti_poison_active_ false after one full cycle.
// Clear pending here so we don't immediately restart another cycle.
if (!this->anti_poison_active_) {
this->anti_poison_pending_ = false;
this->current_mode_ = this->target_mode_;
}
} else {
this->displayed_string_ = this->target_string_;
this->anti_poison_active_ = false;
this->anti_poison_pending_ = false;
this->current_mode_ = this->target_mode_;
}
}
if (this->debug_logging_ && (now_ms - this->last_debug_log_ms_) >= 2000) {
this->last_debug_log_ms_ = now_ms;
const time_t epoch = ::time(nullptr);
auto rtc_now = this->time_source_ != nullptr ? this->time_source_->now() : esphome::ESPTime();
const bool epoch_valid = epoch > 1700000000;
ESP_LOGW(TAG,
"debug status: rtc_configured=%s rtc_valid=%s epoch_valid=%s date_mode=%s auto_rotate=%s epoch=%ld shown=%s",
this->time_source_ != nullptr ? "yes" : "no",
rtc_now.is_valid() ? "yes" : "no",
epoch_valid ? "yes" : "no",
this->date_mode_ ? "on" : "off",
this->auto_rotate_ ? "on" : "off",
(long) epoch,
this->displayed_string_.c_str());
}
// Multiplex scanning must run continuously, independent of poll interval.
const uint32_t now = micros();
if ((uint32_t) (now - this->last_update_us_) >= this->refresh_interval_us_) {
this->last_update_us_ = now;
this->update_tube_display_(this->displayed_string_);
}
}
void NixieDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
// Nixie tubes don't support pixel-based drawing
// This is a text display component
}
void NixieDisplay::set_anode_(uint8_t anode_index) {
this->anode0_pin_->digital_write(false);
this->anode1_pin_->digital_write(false);
this->anode2_pin_->digital_write(false);
if (anode_index == 0)
this->anode0_pin_->digital_write(true);
else if (anode_index == 1)
this->anode1_pin_->digital_write(true);
else if (anode_index == 2)
this->anode2_pin_->digital_write(true);
}
uint8_t NixieDisplay::char_to_digit_(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
return 0;
}
void NixieDisplay::set_target_string_(const std::string &value, DisplayMode mode) {
std::string filtered;
filtered.reserve(NUM_TUBES);
for (char c : value) {
if (c >= '0' && c <= '9') {
filtered.push_back(c);
if (filtered.size() == NUM_TUBES)
break;
}
}
if (filtered.size() < NUM_TUBES)
filtered.append(NUM_TUBES - filtered.size(), '0');
this->target_mode_ = mode;
if (this->anti_poison_on_mode_change_ &&
this->current_mode_ != MODE_UNKNOWN &&
this->target_mode_ != MODE_UNKNOWN &&
this->current_mode_ != this->target_mode_) {
this->anti_poison_pending_ = true;
}
this->target_string_ = filtered;
}
void NixieDisplay::update_tube_display_(const std::string &display_str) {
// Ensure string is exactly 6 characters
if (display_str.length() != NUM_TUBES) {
return;
}
// Update tube pair for current anode set
int cur_tube = this->current_anode_ * 2;
uint8_t digit1 = this->char_to_digit_(display_str[cur_tube]);
uint8_t digit2 = this->char_to_digit_(display_str[cur_tube + 1]);
uint32_t var32 = this->digit_bitmap_[digit1];
uint32_t tmp_var = this->digit_bitmap_[digit2];
var32 |= (tmp_var << 10);
// SPI transfer (3 bytes)
this->set_anode_(255); // blank all anodes while shifting out new cathode data
this->le_pin_->digital_write(false); // Transparent mode
const uint8_t tx[3] = {(uint8_t) (var32 >> 16),
(uint8_t) (var32 >> 8),
(uint8_t) var32};
this->enable();
this->write_array(tx, sizeof(tx));
this->disable();
this->le_pin_->digital_write(true); // Latch data
// Set active anode
this->set_anode_(this->current_anode_);
// Cycle to next anode set
this->current_anode_++;
if (this->current_anode_ >= 3) {
this->current_anode_ = 0;
}
}
std::string NixieDisplay::anti_poison_transition_(const std::string &from_str,
const std::string &to_str) {
static uint8_t current_digits[6];
static uint8_t target_digits[6];
static uint8_t iteration_counter = 0;
if (!this->anti_poison_active_) {
this->anti_poison_active_ = true;
for (int i = 0; i < NUM_TUBES; i++) {
current_digits[i] = this->char_to_digit_(from_str[i]);
target_digits[i] = this->char_to_digit_(to_str[i]);
}
iteration_counter = 0;
}
// Increment all digits to prevent cathode poisoning
for (int i = 0; i < NUM_TUBES; i++) {
if (iteration_counter < 5) {
current_digits[i]++;
} else if (current_digits[i] != target_digits[i]) {
current_digits[i]++;
}
if (current_digits[i] >= 10) {
current_digits[i] = 0;
}
}
iteration_counter++;
if (iteration_counter >= 10) {
iteration_counter = 0;
this->anti_poison_active_ = false;
}
std::string result;
result.reserve(NUM_TUBES);
for (int i = 0; i < NUM_TUBES; i++) {
result += (char)('0' + current_digits[i]);
}
return result;
}
} // namespace nixie_display
} // namespace esphome

View File

@@ -0,0 +1,150 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/time/real_time_clock.h"
#include <cstdarg>
#include <string>
namespace esphome {
namespace nixie_display {
class NixieDisplay : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST,
spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING,
spi::DATA_RATE_200KHZ> {
public:
enum DisplayMode : uint8_t {
MODE_UNKNOWN = 0,
MODE_TIME = 1,
MODE_DATE = 2,
};
void set_anode_pins(InternalGPIOPin *anode0, InternalGPIOPin *anode1,
InternalGPIOPin *anode2, InternalGPIOPin *le_pin) {
this->anode0_pin_ = anode0;
this->anode1_pin_ = anode1;
this->anode2_pin_ = anode2;
this->le_pin_ = le_pin;
}
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
display::DisplayType get_display_type() override {
return display::DISPLAY_TYPE_BINARY;
}
void printf(const char *format, ...);
void display_text(const char *text) {
this->last_external_text_ms_ = millis();
this->set_target_string_(text, MODE_UNKNOWN);
}
void display_time_text(const char *text) {
this->last_external_text_ms_ = millis();
this->set_target_string_(text, MODE_TIME);
}
void display_date_text(const char *text) {
this->last_external_text_ms_ = millis();
this->set_target_string_(text, MODE_DATE);
}
void set_time_source(time::RealTimeClock *rtc) { this->time_source_ = rtc; }
void set_debug_logging(bool enabled) { this->debug_logging_ = enabled; }
void set_anti_poisoning(bool enabled) { this->anti_poisoning_ = enabled; }
void set_lambda_hold_ms(uint32_t hold_ms) { this->lambda_hold_ms_ = hold_ms; }
void set_auto_time(bool enabled) { this->auto_time_ = enabled; }
void set_date_mode(bool enabled) {
this->date_mode_ = enabled;
if (enabled) {
this->auto_rotate_ = false;
}
}
bool is_date_mode() const { return this->date_mode_; }
void set_auto_rotate(bool enabled) {
this->auto_rotate_ = enabled;
if (enabled) {
this->date_mode_ = false;
}
}
bool is_auto_rotate() const { return this->auto_rotate_; }
void set_refresh_interval_us(uint32_t interval_us) {
this->refresh_interval_us_ = interval_us;
}
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_height_internal() override { return 1; }
int get_width_internal() override { return NUM_TUBES; }
private:
InternalGPIOPin *anode0_pin_{nullptr};
InternalGPIOPin *anode1_pin_{nullptr};
InternalGPIOPin *anode2_pin_{nullptr};
InternalGPIOPin *le_pin_{nullptr};
// Bitmap for digits 0-9, 10 bits per digit (cathode pins)
const uint16_t digit_bitmap_[10] = {
1022, // 0
1021, // 1
1019, // 2
1015, // 3
1007, // 4
991, // 5
959, // 6
895, // 7
767, // 8
511 // 9
};
static constexpr int NUM_TUBES = 6;
static constexpr uint32_t DEFAULT_REFRESH_INTERVAL_US = 1000;
uint32_t last_update_us_;
uint32_t last_logic_update_ms_{0};
uint32_t refresh_interval_us_{DEFAULT_REFRESH_INTERVAL_US};
uint32_t lambda_hold_ms_{1500};
uint8_t current_anode_;
std::string displayed_string_;
std::string target_string_;
DisplayMode current_mode_{MODE_UNKNOWN};
DisplayMode target_mode_{MODE_UNKNOWN};
bool anti_poison_active_;
unsigned long anti_poison_start_;
uint8_t anti_poison_counter_;
HighFrequencyLoopRequester high_freq_;
time::RealTimeClock *time_source_{nullptr};
bool debug_logging_{false};
bool anti_poisoning_{true};
bool auto_time_{true};
bool date_mode_{false};
bool auto_rotate_{false};
bool anti_poison_on_mode_change_{true};
bool anti_poison_pending_{false};
uint32_t last_debug_log_ms_{0};
uint32_t last_external_text_ms_{0};
int last_render_second_{-1};
bool last_effective_date_mode_{false};
// Anti-poisoning function: cycles through digits to prevent cathode buildup
std::string anti_poison_transition_(const std::string &from_str,
const std::string &to_str);
// Update the SPI display for current tube pair and anode
void update_tube_display_(const std::string &display_str);
// Set active anode
void set_anode_(uint8_t anode_index);
// Convert character to digit (0-9)
uint8_t char_to_digit_(char c);
void set_target_string_(const std::string &value, DisplayMode mode);
};
} // namespace nixie_display
} // namespace esphome

View File

@@ -0,0 +1,105 @@
spi:
- id: lcd_spi
clk_pin: GPIO48
mosi_pin: GPIO47
i2c:
- id: bus_a
sda: GPIO19
scl:
number: GPIO45
ignore_strapping_warning: true
frequency: 100kHz
output:
# Backlight LED
- platform: ledc
pin: GPIO38
id: backlight_output
frequency: 100Hz
light:
# Backlight
- platform: monochromatic
output: backlight_output
name: Backlight
id: display_backlight
restore_mode: ALWAYS_ON
on_turn_on:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming by backlight on"
- lvgl.resume:
- lvgl.widget.redraw:
on_turn_off:
- if:
condition:
lambda: 'return id(display_timeout_number).state >= 0;'
then:
- logger.log: "Backlight off, pausing LVGL"
- lvgl.pause:
touchscreen:
- platform: gt911
id: my_touchscreen
transform:
mirror_x: false
mirror_y: false
display: my_display
on_release:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: display_backlight
display:
- platform: st7701s
id: my_display
update_interval: never
auto_clear_enabled: false
data_rate: 2MHz
spi_mode: MODE3
color_order: RGB
invert_colors: false
dimensions:
width: 480
height: 480
transform:
mirror_x: false
mirror_y: false
cs_pin: 39
# reset not defined
de_pin: 18
hsync_pin: 16
vsync_pin: 17
pclk_pin: 21
init_sequence:
- 1
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10] # CMD2_BKSEL_BK0
- [0xCD, 0x00] # disable MDT flag
pclk_frequency: 12MHz
pclk_inverted: false
data_pins:
red:
- 11 # R1
- 12 # R2
- 13 # R3
- 14 # R4
- 0 # R5
green:
- 8 # G0
- 20 # G1
- 3 # G2
- 46 # G3
- 9 # G4
- 10 # G5
blue:
- 4 # B1
- 5 # B2
- 6 # B3
- 7 # B4
- 15 # B5

View File

@@ -0,0 +1,66 @@
---
light:
# Backlight
- platform: monochromatic
output: backlight_output
name: Backlight
id: display_backlight
restore_mode: ALWAYS_ON
on_turn_on:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming by backlight on"
- lvgl.resume:
- lvgl.widget.redraw:
on_turn_off:
- if:
condition:
lambda: 'return id(display_timeout_number).state >= 0;'
then:
- logger.log: "Backlight off, pausing LVGL"
- lvgl.pause:
output:
# Backlight LED
- platform: ledc
pin: ${pin_lcd_bl}
id: backlight_output
frequency: 100Hz
esp_ldo:
- channel: 3
voltage: 2.5V
psram:
speed: 200MHz
display:
- platform: mipi_dsi
model: JC8012P4A1
id: my_display
update_interval: 1s
reset_pin: ${pin_lcd_reset}
i2c:
- id: i2c_bus
sda: ${pin_touch_sda}
scl: ${pin_touch_scl}
frequency: 400kHz
touchscreen:
- platform: gsl3680
id: touchscreen_
reset_pin: ${pin_touch_rst}
interrupt_pin: ${pin_touch_irq}
transform:
swap_xy: false
mirror_y: false
on_release:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: display_backlight

View File

@@ -0,0 +1,37 @@
# Backlight
output:
- platform: ledc
pin: $pin_dis_bk
id: gpio_pin_dis_bk_backlight_pwm
light:
- platform: monochromatic
output: gpio_pin_dis_bk_backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
# ST7789 Display
# IPS Panel 1.3 Inch 260ppi 240x240 16-bit full color pixels
spi:
id: bus_tft
clk_pin: ${pin_spi_clk}
mosi_pin: ${pin_spi_mosi}
#miso_pin: $spi_miso
display:
- platform: ili9xxx
model: st7789v
id: my_display
dimensions:
height: 240
width: 240
offset_width: 0
offset_height: 0
invert_colors: true
cs_pin: $pin_dis_cs
dc_pin: $pin_dis_dc
# update_interval: 2s
# # Simple text test
# lambda: |-
# it.printf(0, 0, id(roboto16), "Test");

View File

@@ -1,6 +1,6 @@
substitutions:
name: everything-presence-lite-20946c
friendly_name: Everything Presence Lite 20946c
name: eplite
friendly_name: Everything Presence Lite
packages:
EverythingSmartTechnology.Everything_Presence_Lite: github://everythingsmarthome/everything-presence-lite/everything-presence-lite-ha.yaml@main
esphome:

17
esphome/ep-one.yaml Normal file
View File

@@ -0,0 +1,17 @@
substitutions:
name: everything-presence-one-7f7364
friendly_name: Everything Presence One 7f7364
packages:
Everything Smart Technology.Everything Presence One: github://everythingsmarthome/everything-presence-one/everything-presence-one.yaml@main
esphome:
name: ${name}
name_add_mac_suffix: false
friendly_name: ${friendly_name}
api:
encryption:
key: !secret ep1_one_api
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

138
esphome/esp32-nixie.yaml Normal file
View File

@@ -0,0 +1,138 @@
substitutions:
device_name: "esp32-nixie"
friendly_name: "esp32-nixie"
comment: "esp32"
location: "Woonkamer"
api_password: "RjgVy40E6HfdRtEVN4Uod2315B2yoFA+ilOl1Wf/PwE="
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
gateway: !secret ip_gateway
subnet: !secret ip_subnet
board: "esp32dev"
framework: esp-idf
packages:
board: !include boards/esp32-gen.yaml
device_base: !include common/common.yaml
connection: !include common/wifi.yaml
logger: !include templates/logger.yaml
# logger:
# level: WARN
external_components:
- source:
type: local
path: custom_component
components: [nixie_display]
time:
# - platform: sntp
# id: sntp_time
# servers:
# - 0.pool.ntp.org
# - 1.pool.ntp.org
# - 2.pool.ntp.org
- platform: homeassistant
id: ha_time
spi:
id: nixie_spi
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
display:
- platform: nixie_display
id: nixie_clock
spi_id: nixie_spi
time_id: ha_time
auto_time: true
debug_logging: false
anti_poisoning: true
lambda_hold_ms: 0
update_interval: 200ms
refresh_interval_us: 1000
anode0_pin: GPIO5
anode1_pin: GPIO13
anode2_pin: GPIO17
le_pin: GPIO22
switch:
- platform: template
id: date_mode_switch
name: "Nixie Date Mode"
lambda: |-
return id(nixie_clock).is_date_mode();
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
id(nixie_clock).set_date_mode(true);
turn_off_action:
- lambda: |-
id(nixie_clock).set_date_mode(false);
- platform: template
id: auto_rotate_switch
name: "Nixie Auto Rotate"
lambda: |-
return id(nixie_clock).is_auto_rotate();
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- lambda: |-
id(nixie_clock).set_auto_rotate(true);
turn_off_action:
- lambda: |-
id(nixie_clock).set_auto_rotate(false);
# Buttons from defines.h
binary_sensor:
- platform: gpio
pin:
number: GPIO2
mode: INPUT_PULLUP
name: "Up Button"
on_press:
then:
- logger.log: "Up button pressed"
- platform: gpio
pin:
number: GPIO4
mode: INPUT_PULLUP
name: "Down Button"
on_press:
then:
- logger.log: "Down button pressed"
- platform: gpio
pin:
number: GPIO16
mode: INPUT_PULLUP
name: "Mode Button"
on_press:
then:
- logger.log: "Mode button pressed"
# Underglow LED (RGB) for tube illumination from defines.h
light:
- platform: rgb
name: "Tube Underglow"
red: underglow_led_r
green: underglow_led_g
blue: underglow_led_b
restore_mode: RESTORE_DEFAULT_ON
output:
- platform: ledc
pin: GPIO25
id: underglow_led_r
- platform: ledc
pin: GPIO26
id: underglow_led_g
- platform: ledc
pin: GPIO27
id: underglow_led_b

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
esphome/fonts/icons_v2.ttf Normal file

Binary file not shown.

BIN
esphome/fonts/materialdesignicons-webfont.ttf Executable file → Normal file

Binary file not shown.

View File

@@ -0,0 +1,17 @@
substitutions:
name: "home-assistant-voice-slaapkamer"
friendly_name: Home Assistant Voice Slaapkamer
packages:
Nabu Casa.Home Assistant Voice PE: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml
esphome:
name: ${name}
name_add_mac_suffix: false
friendly_name: ${friendly_name}
api:
encryption:
key: bbVE1fLyViITtfAwb9GmN3Ip6cZPCqIFi/Q3Xlp6XwY=
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password

View File

@@ -1,16 +1,16 @@
substitutions:
device_name: "hvac-ir-zolder"
friendly_name: "Airco IR Zolder"
friendly_name: "IR-blaster Woonkamer"
comment: "ESP8266, Tuya-hack, IR"
location: "zolder"
location: "woonkamer"
api_password: !secret hvac_zolder_api
ota_password: !secret ota_password
wifi_ssid: !secret wifi_ssid
wifi_password: !secret wifi_password
gateway: !secret ip_gateway
subnet: !secret ip_subnet
ip: !secret hvac_zolder_ip
hvac_sensor: sht_temp
#gateway: !secret ip_gateway
#subnet: !secret ip_subnet
#ip: !secret hvac_zolder_ip
#hvac_sensor: sht_temp
update_interval: 30s
#pin definitions
pin_ir_tx: GPIO14
@@ -27,11 +27,13 @@ packages:
i2c_a: !include interfaces/i2c_a.yaml
connection: !include common/wifi.yaml
device_base: !include common/common.yaml
climate: !include templates/climate_sens.yaml
#climate: !include templates/climate_sens.yaml
#status: !include templates/status.yaml
logger: !include templates/logger.yaml
sht3x: !include sensors/sht3x.yaml
smsl: !include templates/remote_smsl.yaml
#smsl: !include templates/remote_smsl.yaml
#kerst: !include templates/remote_kerstlampjes.yaml
beamer: !include templates/remote_beamer_slaapkamer.yaml

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
esphome/img/flags/cs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
esphome/img/flags/de.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
esphome/img/flags/es.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
esphome/img/flags/fi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
esphome/img/flags/fr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
esphome/img/flags/gb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
esphome/img/flags/hu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
esphome/img/flags/id.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
esphome/img/flags/it.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
esphome/img/flags/kr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
esphome/img/flags/nl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Some files were not shown because too many files have changed in this diff Show More