esphome:
name: my-display-encory
friendly_name: 旋钮彩屏
on_boot:
then:
- logger.log: "设备启动中,开启屏幕电源..."
# 1. 唤醒后立即开启屏幕 VCC 供电 (GPIO23 -> HIGH)
- output.turn_on: display_power_pin
- delay: 50ms # 给予屏幕控制器启动时间
# 2. 启动空闲休眠定时器 (120秒)
- script.execute: start_sleep_timer
# 3. 初始化文本传感器
- text_sensor.template.publish:
id: current_display_name
state: "旋钮控制台"
- text_sensor.template.publish:
id: current_display_state
state: "未知"
- text_sensor.template.publish:
id: button_action_sensor
state: "无"
- light.turn_on: back_light
# 4. 启动时发布 MQTT 切换事件,让 Node-RED 推送初始 UI
- mqtt.publish:
topic: "esphome/display/mode_switch_intent"
payload: "INIT"
qos: 1
retain: true
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
logger:
level: DEBUG
baud_rate: 1159200
api:
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
ap:
ssid: "my-display-encory"
password: ""
web_server:
port: 80
mqtt:
id: mqtt_client
broker: 192.168.*.*
port: 1883
username: *****
password: *****
captive_portal:
preferences:
flash_write_interval: 5min
# ----------------------------------------------------
# 深度休眠组件 (最终修正:使用 EXT1 匹配 HIGH->LOW 唤醒逻辑)
# ----------------------------------------------------
deep_sleep:
id: deep_sleep_1
# 您的唤醒逻辑:空闲 HIGH (3.3V) -> 唤醒 LOW (0V)。
# 【关键】使用 ESP32-S3 专用的 EXT1 唤醒机制
esp32_ext1_wakeup:
pins:
- number: GPIO4
mode: INPUT_PULLDOWN
mode: ANY_HIGH # 监听 LOW 电平唤醒(与按钮按下时的 0V 匹配)
# ====================================================================
# 字体 / 全局变量
# ====================================================================
font:
- file: "MaterialDesignIconsDesktop.ttf"
id: mdi_42
size: 42
bpp: 4
glyphs:
- "\U000F0335" # Lightbulb on
- "\U000F0336" # Lightbulb off
- "\U000F01BE" # Curtain (部分展开/默认)
- "\U000F01BF" # Curtain Closed (关闭)
- "\U000F01BD" # Curtain Open (打开)
- file: "MaterialDesignIconsDesktop.ttf"
id: mdi_72
size: 72
bpp: 4
glyphs:
- "\U000F0335"
- "\U000F0336"
- "\U000F01BE"
- "\U000F01BF"
- "\U000F01BD"
- file: "NotoSansSC-Regular.ttf"
id: zh_cn_18
size: 18
bpp: 4
glyphs:
- "0123456789%.: ONF"
- "设置信息温度湿时间电费水灌岸卡室号内地址按钮操作旋值状态元开关显示器实体单击双长顺逆针三当前未连接知位离线测试色灯启闭打错误备百分比已/!控制台冷暖客厅主卧次B大窗帘名称"
globals:
- id: button_press_start
type: int
initial_value: '0'
restore_value: no
- id: button_is_pressed
type: bool
initial_value: 'false'
restore_value: no
- id: rotation_in_progress
type: bool
initial_value: 'false'
restore_value: no
- id: single_click_counter
type: int
initial_value: '0'
- id: double_click_counter
type: int
initial_value: '0'
- id: triple_click_counter
type: int
initial_value: '0'
# ====================================================================
# 显示配置 / OUTPUT / LIGHT
# ====================================================================
spi:
id: spi_bus_0
clk_pin: GPIO12
mosi_pin: GPIO13
display:
- platform: mipi_spi
id: rotary_display
model: GC9A01A
spi_id: spi_bus_0
cs_pin: GPIO10
dc_pin: GPIO9
reset_pin: GPIO8
dimensions:
width: 240
height: 240
rotation: 90
invert_colors: false
lvgl:
id: lvgl_display
displays:
- rotary_display
on_ready:
then:
- lambda: |-
#include "lvgl.h"
lv_obj_set_style_bg_color(lv_scr_act(), lv_color_make(0xFF, 0xFF, 0xFF), 0);
lv_obj_set_style_bg_opa(lv_scr_act(), LV_OPA_COVER, 0);
lv_obj_set_style_arc_color(id(main_arc), lv_color_make(0xDF, 0xDF, 0xDF), LV_PART_MAIN);
lv_obj_set_style_arc_width(id(main_arc), 15, LV_PART_MAIN);
lv_obj_set_style_arc_color(id(main_arc), lv_color_make(0x00, 0x66, 0xFF), LV_PART_INDICATOR);
lv_obj_set_style_arc_width(id(main_arc), 15, LV_PART_INDICATOR);
lv_obj_set_style_bg_opacity(id(main_arc), 0, LV_PART_KNOB);
lv_obj_set_style_border_width(id(main_arc), 0, LV_PART_KNOB);
widgets:
- arc:
id: main_arc
width: 220
height: 220
align: center
min_value: 0
max_value: 100
value: 0
- label:
id: label_icon
text: "\U000F0336"
align: center
text_font: mdi_72
text_color: 0x7F7F7F
recolor: true
- label:
id: label_name
text: "设备名称"
align: bottom_mid
y: -40
text_font: zh_cn_18
text_color: 0x7F7F7F
- label:
id: label_percentage
text: "0%"
align: bottom_mid
y: -15
text_font: zh_cn_18
text_color: 0x7F7F7F
output:
- platform: ledc
pin: GPIO14
id: backlight_output
inverted: true
- platform: gpio
pin: GPIO7
id: display_power_pin
light:
- platform: monochromatic
output: backlight_output
name: "显示屏背光"
id: back_light
# restore_mode: ALWAYS_ON
# ====================================================================
# 传感器和逻辑配置
# ====================================================================
text_sensor:
- platform: template
name: "通用控制实体友好名称"
id: current_display_name
- platform: template
name: "通用实体状态"
id: current_display_state
- platform: template
name: "按钮动作"
id: button_action_sensor
- platform: template
name: "旋转度"
id: rotation_direction_sensor
# --- WiFi 辅助传感器 (省略) ---
- platform: wifi_info
ip_address:
name: "设备IP地址"
update_interval: 60s
mac_address:
name: "设备MAC地址"
id: mac_address_sensor
- platform: wifi_info
ssid:
name: "WiFi名称"
update_interval: 60s
- platform: wifi_info
bssid:
name: "路由器MAC地址"
update_interval: 60s
# MQTT 订阅:用于接收名称
- platform: mqtt_subscribe
name: "MQTT 名称输入"
id: mqtt_name_input
topic: "esphome/display/name_output"
qos: 0
internal: true
on_value:
- text_sensor.template.publish: { id: current_display_name, state: !lambda 'return x;' }
- lvgl.label.update: { id: label_name, text: !lambda 'return x;' }
# MQTT 订阅:用于接收状态 (V70 修复点)
- platform: mqtt_subscribe
name: "MQTT 状态输入"
id: mqtt_state_input
topic: "esphome/display/state_output"
qos: 0
internal: true
on_value:
- script.stop: start_sleep_timer
- script.execute: start_sleep_timer
- if:
condition:
lambda: 'return !id(rotation_in_progress);'
then:
- text_sensor.template.publish: { id: current_display_state, state: !lambda 'return x;' }
- lambda: |-
#include "lvgl.h"
#include <algorithm> // 引入算法库
#include <cctype> // 引入字符类型库
lv_color_t active_color = lv_color_make(0x00, 0x66, 0xFF);
lv_color_t inactive_color = lv_color_make(0x7F, 0x7F, 0x7F);
std::string input_state = x;
std::string current_name = id(current_display_name).state;
std::string lower_state = input_state;
std::transform(lower_state.begin(), lower_state.end(), lower_state.begin(),
[](unsigned char c){ return std::tolower(c); });
bool is_on_string = (lower_state == "开启" || lower_state == "打开" || lower_state == "on" || lower_state == "open" || lower_state == "opening" || lower_state == "closing");
bool is_off_string = (lower_state == "关闭" || lower_state == "off" || lower_state == "closed");
int incoming_int = atoi(input_state.c_str());
// 窗帘判断标志
bool is_curtain = (current_name.find("窗帘") != std::string::npos);
// --- 逻辑分支 ---
// 1. 关闭/不活动/0% (灰色)
if (is_off_string || (incoming_int == 0 && !is_on_string)) {
lv_arc_set_value(id(main_arc), 0);
lv_label_set_text(id(label_percentage), "0%");
lv_obj_set_style_text_color(id(label_icon), inactive_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_name), inactive_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_percentage), inactive_color, LV_PART_MAIN);
if (is_curtain) {
// 窗帘关闭时使用完全关闭的图标
lv_label_set_text(id(label_icon), "\U000F01BF");
} else {
lv_label_set_text(id(label_icon), "\U000F0336"); // 灯光关闭图标
}
// 2. 百分比 (1-100%) (蓝色)
} else if (incoming_int >= 1 && incoming_int <= 100) {
lv_arc_set_value(id(main_arc), incoming_int);
lv_label_set_text(id(label_percentage), (to_string(incoming_int) + "%").c_str());
lv_obj_set_style_text_color(id(label_icon), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_name), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_percentage), active_color, LV_PART_MAIN);
if (is_curtain) {
// 窗帘有百分比时使用部分展开的图标
lv_label_set_text(id(label_icon), "\U000F01BE");
} else {
lv_label_set_text(id(label_icon), "\U000F0335"); // 灯光开启图标
}
// 3. 开启/打开/Open/Opening/Closing 状态 (蓝色)
} else if (is_on_string) {
int current_arc_value = lv_arc_get_value(id(main_arc));
int default_value = 50;
if (current_arc_value == 0) {
lv_arc_set_value(id(main_arc), default_value);
lv_label_set_text(id(label_percentage), (to_string(default_value) + "%").c_str());
}
lv_obj_set_style_text_color(id(label_icon), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_name), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_percentage), active_color, LV_PART_MAIN);
if (is_curtain) {
// 窗帘开启/活动时,如果百分比大于0,则显示部分展开的图标
// 否则(例如只有“打开”状态,没有百分比),也显示部分展开的图标
if (lv_arc_get_value(id(main_arc)) > 0) {
lv_label_set_text(id(label_icon), "\U000F01BE"); // 部分展开
} else {
lv_label_set_text(id(label_icon), "\U000F01BD"); // 全开图标 (更符合打开/开启的语义)
}
} else {
lv_label_set_text(id(label_icon), "\U000F0335"); // 灯光开启图标
}
}
lv_obj_invalidate(lv_scr_act());
sensor:
# --- 旋转编码器 (保持不变) ---
- platform: rotary_encoder
name: "旋钮值"
id: rotary_sensor
pin_a:
number: GPIO2
inverted: True
mode:
input: true
pullup: true
pin_b:
number: GPIO5
inverted: True
mode:
input: true
pullup: true
min_value: 0
max_value: 25
resolution: 1
publish_initial_value: true
filters:
- debounce: 50ms
- delta: 1.0
- throttle: 100ms
on_value:
- globals.set: { id: rotation_in_progress, value: 'true' }
- script.stop: rotation_end_timer
- script.execute: rotation_end_timer
- script.stop: start_sleep_timer
- script.execute: start_sleep_timer
# UI 更新逻辑 (只基于旋钮值更新百分比和弧度)
- lambda: |-
#include "lvgl.h"
int rotary_state = (int)id(rotary_sensor).state;
int display_value = rotary_state * 4;
std::string value_str = to_string(display_value);
std::string text = value_str + "%";
lv_color_t active_color = lv_color_make(0x00, 0x66, 0xFF);
lv_color_t inactive_color = lv_color_make(0x7F, 0x7F, 0x7F);
lv_arc_set_value(id(main_arc), display_value);
lv_label_set_text(id(label_percentage), text.c_str());
if (display_value > 0) {
lv_obj_set_style_text_color(id(label_icon), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_name), active_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_percentage), active_color, LV_PART_MAIN);
} else {
lv_obj_set_style_text_color(id(label_icon), inactive_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_name), inactive_color, LV_PART_MAIN);
lv_obj_set_style_text_color(id(label_percentage), inactive_color, LV_PART_MAIN);
}
lv_obj_invalidate(lv_scr_act());
on_clockwise:
then:
- if:
condition:
- lambda: 'return !id(button_is_pressed) && (millis() - id(button_press_start) > 20);'
then:
- text_sensor.template.publish: { id: rotation_direction_sensor, state: "顺时针" }
on_anticlockwise:
then:
- if:
condition:
- lambda: 'return !id(button_is_pressed) && (millis() - id(button_press_start) > 20);'
then:
- text_sensor.template.publish: { id: rotation_direction_sensor, state: "逆时针" }
- platform: wifi_signal
name: "WiFi信号强度"
update_interval: 60s
unit_of_measurement: "dBm"
device_class: signal_strength
entity_category: diagnostic
# ====================================================================
# 脚本:用于定时器和锁的释放 (保持不变)
# ====================================================================
script:
- id: start_sleep_timer
mode: restart
then:
- delay: 120s
- logger.log: "设备空闲 120s,准备关闭背光、断开屏幕电源并进入深度休眠。"
- light.turn_off: back_light
- wifi.disable:
- output.turn_off: display_power_pin
- delay: 50ms
- deep_sleep.enter: deep_sleep_1
- id: rotation_end_timer
mode: restart
then:
- delay: 500ms
- globals.set: { id: rotation_in_progress, value: 'false' }
- logger.log: "旋转停止,MQTT 状态同步锁已释放。"
# ====================================================================
# 按钮配置 (保持不变)
# ====================================================================
binary_sensor:
- platform: gpio
name: "主按钮"
id: main_button
pin:
number: GPIO6
inverted: true
mode:
input: true
pullup: true
filters:
- delayed_on: 100ms
- delayed_off: 100ms
on_press:
then:
- globals.set: { id: button_press_start, value: !lambda 'return millis();' }
- globals.set: { id: button_is_pressed, value: 'true' }
- script.stop: start_sleep_timer
- logger.log: "按钮按下,停止休眠定时器"
on_release:
then:
- globals.set: { id: button_is_pressed, value: 'false' }
- logger.log: "按钮释放,重新启动休眠定时器"
- script.execute: start_sleep_timer
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at least 0.5s
then:
- logger.log: "检测到单击"
- lambda: |
id(single_click_counter)++;
id(button_action_sensor).publish_state("单击_" + to_string(id(single_click_counter)));
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.2s
then:
- logger.log: "检测到双击"
- lambda: |
id(double_click_counter)++;
id(button_action_sensor).publish_state("双击_" + to_string(id(double_click_counter)));
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.2s
then:
- logger.log: "检测到三击"
- lambda: |
id(triple_click_counter)++;
id(button_action_sensor).publish_state("三击_" + to_string(id(triple_click_counter)));
- timing:
- ON for 1s to 2s
- OFF for at least 0.5s
then:
- logger.log: "检测到长按"
- text_sensor.template.publish: { id: button_action_sensor, state: "长按" }
# --- 辅助传感器和开关 ---
switch:
- platform: restart
name: "设备重启"