From 60e5cc11818dc15537c361e6debcf06ccb6908b0 Mon Sep 17 00:00:00 2001 From: Darsey Litzenberger Date: Sat, 21 Mar 2026 02:57:43 -0600 Subject: [PATCH] Add support for Lanbon L9-HF smart switch/ceiling fan speed controller --- lanbon-l9-hf.yaml | 522 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 522 insertions(+) create mode 100644 lanbon-l9-hf.yaml diff --git a/lanbon-l9-hf.yaml b/lanbon-l9-hf.yaml new file mode 100644 index 0000000..610c5cc --- /dev/null +++ b/lanbon-l9-hf.yaml @@ -0,0 +1,522 @@ +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 +api: + encryption: + key: !secret lanbon-l9-hf-1.hass_api_key + + 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 + +# TODO: test to make sure bluetooth proxy doesn't cause boot looping! +# 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: + +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 + +lvgl: + id: lvgl_id + displays: + - display_id + touchscreens: + - touchscreen_id + 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 + 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