substitutions: calibration_touchscreen_x_min: 0 calibration_touchscreen_y_min: 0 calibration_touchscreen_x_max: 169 calibration_touchscreen_y_max: 319 calibration_pulse_power_meter: 0.07874192291723468 dashboard_import: package_import_url: github://dlitz/esphome-configs-dlitz/lanbon-l9-hf.yaml@${git_branch} packages: - !include common/esp32s3.yaml - !include common/cpu-temperature.yaml - !include common/factory-reset.yaml - !include common/flash-write-interval.yaml - !include common/git-branch.yaml - !include common/ota.yaml - !include common/ota-update-password.yaml - !include common/restart.yaml - !include common/safe-mode.yaml - !include common/time.yaml - !include common/uptime-info.yaml - !include common/wifi-info.yaml esp32: cpu_frequency: 240MHz flash_size: 16MB esphome: name: lanbon-l9-hf friendly_name: Lanbon L9-HF comment: "Lanbon L9-HF smart switch/ceiling fan speed controller" name_add_mac_suffix: true project: name: "dlitz.lanbon-l9-hf" version: "v0.0.0" on_boot: # Fan initialization priority: 800 then: - script.execute: fan_uart_init psram: speed: 80MHz mode: quad # Enable logging logger: # level: DEBUG # baud_rate: 115200 hardware_uart: uart0 # Enable Home Assistant API with dynamic key api: encryption: on_client_connected: - lvgl.label.update: id: label_apistatus text_color: 0x00FF00 on_client_disconnected: - lvgl.label.update: id: label_apistatus text_color: 0xFF0000 wifi: # ssid: !secret wifi_ssid # password: !secret wifi_password # fast_connect: true min_auth_mode: WPA2 enable_btm: true enable_rrm: true # Enable fallback hotspot (captive portal) in case wifi connection fails ap: password: !secret default_ap_password captive_portal: network: enable_ipv6: true # min_ipv6_addr_count: 2 esp32_improv: authorizer: authorize_improv_over_ble_binary_sensor esp32_ble_tracker: scan_parameters: active: false bluetooth_proxy: active: false time: - id: !extend ${time_source} on_time: - hours: 23,4 minutes: 5 seconds: 0 then: - switch.turn_on: switch_antiburn - hours: 23,4 minutes: 35 seconds: 0 then: - switch.turn_off: switch_antiburn - seconds: 0 minutes: '*' then: - lvgl.label.update: id: label_time text: time_format: "%l:%M %p" time: ${time_source} uart: - id: fan_uart tx_pin: GPIO38 baud_rate: 115200 debug: direction: TX after: bytes: 4 script: - id: fan_uart_command parameters: cmd: int arg: int then: - lambda: |- uint8_t buf[4] = { 0xef, uint8_t(cmd), uint8_t(arg), 0}; buf[3] = buf[0] ^ buf[1] ^ buf[2]; // checksum id(fan_uart).write_array(buf, sizeof(buf)); - id: fan_uart_init then: - script.execute: id: fan_uart_command cmd: 0x01 arg: 0x4e - id: fan_uart_set_speed parameters: percent: uint8_t then: - lambda: |- if (percent > 100) { ESP_LOGW("_percent", "called with too-large value %d%%", (int)percent); percent = 100; } id(fan_uart_command)->execute(0x02, percent); output: - platform: ledc id: lcd_backlight_output pin: number: GPIO42 inverted: false - platform: gpio id: relay_output pin: number: GPIO36 - platform: template id: fan_output type: float write_action: - lambda: |- uint8_t speed = ::round(state); const uint8_t speed_percentages[4] = { 0, 20, 55, 100 }; if (speed < 4) { uint8_t percent = speed_percentages[speed]; id(fan_uart_set_speed)->execute(percent); } fan: - id: fan_id name: Fan platform: speed output: fan_output restore_mode: RESTORE_DEFAULT_OFF speed_count: 3 on_state: - lvgl.widget.update: id: fan_speed_0_button state: checked: !lambda 'return !x->state && id(highlight_fan_off_button).state;' - lvgl.widget.update: id: fan_speed_1_button state: checked: !lambda 'return x->state && x->speed == 1;' - lvgl.widget.update: id: fan_speed_2_button state: checked: !lambda 'return x->state && x->speed == 2;' - lvgl.widget.update: id: fan_speed_3_button state: checked: !lambda 'return x->state && x->speed == 3;' binary_sensor: - platform: gpio name: Proximity Sensor id: proximity_sensor pin: GPIO11 on_press: then: - if: condition: lvgl.is_paused then: - logger.log: "LVGL resuming due to proximity sensor." - lvgl.resume: - lvgl.widget.redraw: - light.turn_on: display_backlight - platform: template id: lvgl_paused_sensor_id name: LVGL Paused entity_category: diagnostic condition: lvgl.is_paused: - platform: template id: authorize_improv_over_ble_binary_sensor name: Authorizing Improv-over-BLE entity_category: diagnostic internal: true light: - platform: monochromatic id: display_backlight name: Display Backlight output: lcd_backlight_output restore_mode: ALWAYS_ON entity_category: diagnostic spi: type: octal id: display_spi clk_pin: GPIO21 data_pins: - GPIO6 - GPIO7 - GPIO15 - GPIO16 - GPIO10 - GPIO9 - ignore_strapping_warning: true number: GPIO46 - ignore_strapping_warning: true number: GPIO3 display: - platform: mipi_spi id: display_id model: st7789v spi_id: display_spi bus_mode: octal dc_pin: GPIO17 dimensions: height: 320 width: 170 offset_height: 0 offset_width: 35 invert_colors: true show_test_card: false data_rate: 4MHz color_depth: 16BIT color_order: BGR auto_clear_enabled: false update_interval: 250ms i2c: - id: touchscreen_i2c sda: GPIO35 scl: number: GPIO0 ignore_strapping_warning: true scan: true touchscreen: - platform: ft63x6 id: touchscreen_id display: display_id i2c_id: touchscreen_i2c calibration: x_min: ${calibration_touchscreen_x_min} y_min: ${calibration_touchscreen_y_min} x_max: ${calibration_touchscreen_x_max} y_max: ${calibration_touchscreen_y_max} on_release: - if: condition: lvgl.is_paused then: - logger.log: "LVGL resuming due to screen touch." - lvgl.resume: - lvgl.widget.redraw: - light.turn_on: display_backlight sensor: - platform: pulse_meter name: Power id: power_pulse_meter pin: GPIO5 unit_of_measurement: 'W' device_class: power state_class: measurement internal_filter_mode: PULSE accuracy_decimals: 1 filters: - filter_out: nan - throttle: 15s - multiply: ${calibration_pulse_power_meter} total: name: "Total Pulses" # DEBUG - platform: total_daily_energy id: energy name: "Energy" restore: false power_id: power_pulse_meter unit_of_measurement: "kWh" state_class: total_increasing device_class: energy accuracy_decimals: 3 filters: # Convert W to kW - multiply: 0.001 switch: - platform: output name: Relay id: relay output: relay_output restore_mode: RESTORE_DEFAULT_OFF on_turn_on: - lvgl.widget.update: id: light_toggle_button state: checked: true on_turn_off: - lvgl.widget.update: id: light_toggle_button state: checked: false - platform: template name: Antiburn id: switch_antiburn icon: mdi:television-shimmer optimistic: true entity_category: diagnostic turn_on_action: - logger.log: "Starting Antiburn" - if: condition: lvgl.is_paused then: - lvgl.resume: - lvgl.widget.redraw: - lvgl.pause: show_snow: true - light.turn_off: display_backlight turn_off_action: - logger.log: "Stopping Antiburn" - if: condition: lvgl.is_paused then: - lvgl.resume: - lvgl.widget.redraw: - platform: template id: highlight_fan_off_button name: Highlight Fan-Off Button optimistic: True entity_category: config restore_mode: RESTORE_DEFAULT_ON font: - # file: "https://github.com/Pictogrammers/pictogrammers.github.io/raw/refs/heads/main/@mdi/font/7.4.47/fonts/materialdesignicons-webfont.ttf" file: "fonts/pictogrammers/@mdi/font/7.4.47/fonts/materialdesignicons-webfont.ttf" id: mdi_icons bpp: 4 size: 52 glyphs: - "\U000F0336" # mdi:lightbulb-outline - "\U000F06E9" # mdi:lightbulb-on-outline - "\U000F081D" # mdi:fan-off - "\U000F1472" # mdi:fan-speed-1 - "\U000F1473" # mdi:fan-speed-2 - "\U000F1474" # mdi:fan-speed-3 - file: "gfonts://Roboto" id: roboto_20 size: 20 lvgl: id: lvgl_id displays: - display_id touchscreens: - touchscreen_id: touchscreen_id long_press_repeat_time: 100ms buffer_size: 25% log_level: WARN on_idle: timeout: 30s then: - logger.log: "LVGL is idle" - light.turn_off: display_backlight - lvgl.pause: style_definitions: - id: header_footer bg_color: 0x2F8CD8 bg_grad_color: 0x005782 bg_grad_dir: VER bg_opa: COVER border_opa: TRANSP radius: 0 pad_all: 0 pad_row: 0 pad_column: 0 border_color: 0x0077b3 text_color: 0xFFFFFF width: 100% height: 30 - id: factory_reset_button_style bg_color: 0xFF0000 top_layer: widgets: - obj: align: TOP_MID styles: header_footer widgets: - label: text: time_format: "%l:%M %p" time: ${time_source} id: label_time align: CENTER text_align: CENTER text_color: 0xFFFFFF - label: text: "\uF1EB" id: label_apistatus align: top_right x: -2 y: 7 text_align: right text_color: 0xFFFFFF - buttonmatrix: align: bottom_mid styles: header_footer pad_all: 0 outline_width: 0 id: top_layer items: styles: header_footer rows: - buttons: - id: page_prev text: "\uF053" on_press: then: lvgl.page.previous: - id: page_home text: "\uF015" on_press: then: lvgl.page.show: page_main - id: page_next text: "\uF054" on_press: then: lvgl.page.next: pages: - id: page_main pad_all: 0 widgets: - buttonmatrix: id: matrix_id width: 100% height: 260 x: 0 y: 30 pad_top: 2 pad_bottom: 0 pad_left: 2 pad_right: 0 pad_row: 3 pad_column: 1 text_font: mdi_icons rows: - buttons: - id: light_toggle_button text: "\U000F0336" # mdi:lightbulb-outline width: 2 control: checkable: true on_click: - switch.toggle: relay - buttons: - id: fan_speed_0_button text: "\U000F081D" # mdi:fan-off control: checkable: true on_click: - fan.turn_off: fan_id - id: fan_speed_1_button text: "\U000F1472" # mdi:fan-speed-1 control: checkable: true on_click: - fan.turn_on: id: fan_id speed: 1 - buttons: - id: fan_speed_2_button text: "\U000F1473" # mdi:fan-speed-2 control: checkable: true on_click: - fan.turn_on: id: fan_id speed: 2 - id: fan_speed_3_button text: "\U000F1474" # mdi:fan-speed-3 control: checkable: true on_click: - fan.turn_on: id: fan_id speed: 3 - id: page_diagnostics widgets: - button: width: 164 height: 100 x: 2 y: 58 id: factory_reset_lvgl_button styles: factory_reset_button_style on_release: - if: condition: lambda: "return id(factory_reset_lvgl_button_longpress_ms) >= 10000;" then: - light.turn_off: display_backlight - button.press: button_factory_reset else: - lambda: |- id(factory_reset_lvgl_button_longpress_ms) = 0; - lvgl.style.update: id: factory_reset_button_style bg_color: 0xFF0000 text_color: 0xFFFFFFF on_press: - lambda: |- id(factory_reset_lvgl_button_longpress_ms) = 0; on_long_press_repeat: - logger.log: format: "Long press repeat %d ms" args: ["id(factory_reset_lvgl_button_longpress_ms)"] - lambda: |- id(factory_reset_lvgl_button_longpress_ms) += 100; - if: condition: lambda: "return id(factory_reset_lvgl_button_longpress_ms) == 10000;" then: - lvgl.style.update: id: factory_reset_button_style bg_color: 0xFFFF00 text_color: 0xFF0000 - lvgl.widget.redraw: factory_reset_lvgl_button widgets: - label: id: factory_reset_lvgl_button_label align: CENTER text: "Hold 10s\nfor Factory Reset" text_align: CENTER text_font: roboto_20 - button: width: 164 height: 100 x: 2 y: 162 id: factory_reset_lvgl_cancel_button on_release: - lambda: |- id(factory_reset_lvgl_button_longpress_ms) = 0; - lvgl.page.show: page_main widgets: - label: id: factory_reset_lvgl_cancel_button_label align: CENTER text: "Cancel" text_align: CENTER text_font: roboto_20 - id: page_setup widgets: - button: width: 164 height: 100 x: 2 y: 58 id: authorize_improv_over_ble_lvgl_button on_press: - logger.log: "Authorizing Improv-over-BLE" - binary_sensor.template.publish: id: authorize_improv_over_ble_binary_sensor state: ON on_release: - binary_sensor.template.publish: id: authorize_improv_over_ble_binary_sensor state: OFF widgets: - label: id: authorize_improv_over_ble_lvgl_button_label align: CENTER text: "Improv-over-BLE\nAuthorize" text_align: CENTER text_font: roboto_20 - button: width: 164 height: 100 x: 2 y: 162 id: page_setup_lvgl_cancel_button on_release: - lvgl.page.show: page_main widgets: - label: id: page_setup_lvgl_cancel_button_label align: CENTER text: "Cancel" text_align: CENTER text_font: roboto_20 globals: - id: factory_reset_lvgl_button_longpress_ms type: int restore_value: no initial_value: '0'