Lanbon L9 LCD Smart Light Switch with Motion Sensor

Hi, just wanted to share that I managed to get the board version L9-HS_CB_V_V3.3 20250319 working (bought from Amazon in Jan 2026). This template is modified based on templates provided by lylavoie (Lincoln Lavoie) · GitHub and dlitz (Darsey Litzenberger) · GitHub. All credits to them.

Here’s the yaml template config for the 5 button layout seen:

# Lanbon L9-HF
# MAC: 

esphome:
  name: lanbonl9-porch-entry
  friendly_name: Porch Entry Keypad

esp32:
  board: esp-wrover-kit
  framework:
    type: esp-idf

# Enable logging
logger:
  level: DEBUG

api:
  encryption:
    key: !secret living_keypad_api_key
  reboot_timeout: 0s
  on_client_connected:
    - lvgl.label.update:
        id: label_apistatus
        text_color: 0x00FF00
  on_client_disconnected:
    - lvgl.label.update:
        id: label_apistatus
        text_color: 0xFF0000

ota:
  - platform: esphome
    password: !secret living_keypad_ota_password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  manual_ip:
    static_ip: 10.0.0.100
    gateway: 10.0.0.1
    subnet: 255.255.255.0
    dns1: 10.0.0.1

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "living_keypad Fallback"
    password: !secret living_keypad_ap_password

psram:
  mode: quad
  speed: 80MHz

time:
  - platform: homeassistant
  #- platform: sntp
  #  timezone: America/Edmonton
    id: current_time
    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: current_time

output:
  - platform: ledc
    pin: GPIO21
    id: lcd_backlight_output

light:
  - platform: monochromatic
    name: "Display Backlight"
    id: display_backlight
    output: lcd_backlight_output
    restore_mode: ALWAYS_ON

i2c:
  - id: touchscreen_i2c
    sda: GPIO4
    scl:
      number: GPIO0
      ignore_strapping_warning: true
    frequency: 100kHz
    scan: true

spi:
  id: display_spi
  clk_pin: GPIO26
  mosi_pin: GPIO14

display:
  - id: my_display
    platform: mipi_spi
    model: st7789v
    spi_id: display_spi
    bus_mode: single
    dc_pin: GPIO27
    cs_pin: GPIO25
    dimensions:
      height: 320
      width: 170
      offset_height: 0
      offset_width: 35
    invert_colors: true
    show_test_card: false
    data_rate: 40MHz
    color_depth: 16BIT
    color_order: BGR
    auto_clear_enabled: false
    update_interval: 250ms

touchscreen:
  - platform: ft63x6
    id: my_touchscreen
    display: my_display
    i2c_id: touchscreen_i2c
    calibration:
      x_min: 0
      y_min: 0
      x_max: 169
      y_max: 319
    on_release:
      - if:
          condition: lvgl.is_paused
          then:
            - 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: wifi_signal
    name: Wifi Signal
    update_interval: 60s
    entity_category: diagnostic

  - platform: uptime
    name: Uptime

  - platform: internal_temperature
    name: "Internal Temperature"

  - platform: pulse_meter
    name: Power
    id: power_pulse_meter
    pin: GPIO35
    unit_of_measurement: 'W'
    device_class: power
    state_class: measurement
    internal_filter_mode: PULSE
    accuracy_decimals: 1
    filters:
      - filter_out: nan
      - throttle: 15s
      - multiply: 0.0813287514318442  # Calibration may be needed

binary_sensor:
  - platform: gpio
    name: "Proximity Sensor"
    id: proximity_sensor
    pin: GPIO15
    device_class: motion
#   filters:
#     - delayed_on_off:
#         time_on: 1s
#         time_off: 60s
    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

#    on_release: 
#      - logger.log: "No motion detected"
#      - light.turn_off: display_backlight
#      - lvgl.pause:

  - platform: homeassistant
    id: stairs_state
    entity_id: switch.1_stairs
    attribute: group_all_on
    trigger_on_initial_state: true
    on_state:
      then:
        lvgl.widget.update:
          id: stair_switch_button
          state:
            checked: !lambda return x;

  - platform: homeassistant
    id: garage_state
    entity_id: switch.1_garage
    attribute: group_all_on
    trigger_on_initial_state: true
    on_state:
      then:
        lvgl.widget.update:
          id: garage_switch_button
          state:
            checked: !lambda return x;

  - platform: homeassistant
    id: living_state
    entity_id: switch.2_living_room
    attribute: group_all_on
    trigger_on_initial_state: true
    on_state:
      then:
        lvgl.widget.update:
          id: living_switch_button
          state:
            checked: !lambda return x;

  - platform: homeassistant
    id: kitchen_state
    entity_id: switch.2_kitchen
    attribute: group_all_on
    trigger_on_initial_state: true
    on_state:
      then:
        lvgl.widget.update:
          id: kitchen_switch_button
          state:
            checked: !lambda return x;

switch:
  - platform: gpio
    name: Relay
    id: relay
    pin:
      number: GPIO32
    on_turn_on:
      - lvgl.widget.update:
          id: light_switch
          state:
            checked: true
      - lvgl.label.update:
          id: label_light_switch
#          text: "\U000F06E9"      #  lightbulb lit
          text: "\U000F1054"       #  outdoor lamp
          text_color: 0xFFEE00
    on_turn_off:
      - lvgl.widget.update:
          id: light_switch
          state:
            checked: false
      - lvgl.label.update:
          id: label_light_switch
#          text: "\U000F0336"      #  lightbulb unlit
          text: "\U000F1054"       #  outdoor lamp
          text_color: 0xFFFFFF

  - platform: template
    name: Antiburn
    id: switch_antiburn
    icon: mdi:television-shimmer
    optimistic: true
    #entity_category: config
    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:

font:
  - file: "https://github.com/Pictogrammers/pictogrammers.github.io/raw/refs/heads/main/@mdi/font/7.4.47/fonts/materialdesignicons-webfont.ttf"
    id: mdi_icons
    bpp: 4
    size: 52
    glyphs: [
      "\U000F0336",
      "\U000F06E9",
      "\U000F0D35",
      "\U000F1054"
    ]

lvgl:
  displays:
    - my_display
  touchscreens:
    - my_touchscreen
  bg_color: 0x000000
  buffer_size: 25%
  log_level: WARN
#  styles:
#    - id: btn_primary
#      bg_color: 0x2F8CD8
#      text_color: 0xFFFFFF
#      pressed:
#        bg_color: 0x31302F
#    - id: label_style
#      text_color: 0xFFFFFF
#      text_align: CENTER
  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_color: 0x000000
#      bg_grad_color: 0x005782
      bg_grad_color: 0x000000
      bg_grad_dir: VER
      bg_opa: COVER
      border_opa: TRANSP
      radius: 0
      pad_all: 0
      pad_row: 0
      pad_column: 0
#      border_color: 0x0077b3
      border_color: 0x000000
      text_color: 0xFFFFFF
      width: 100%
      height: 30

    - id: button_default
      bg_color: 0x6F7072        # Your Slate Gray
      bg_opa: COVER
      radius: 5
      text_color: 0xFFFFFF

    - id: button_pressed
      bg_color: 0x00F7FC        # Your Electric Cyan
      bg_opa: COVER
      radius: 5
      text_color: 0xFFFFFF

  top_layer:
    widgets:
      - obj:
          align: TOP_MID
          styles: header_footer
          widgets:
          - label:
              text:
                time_format: "%l:%M %p"
                time: current_time
              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_light_switch
              - id: page_next
                text: "\uF054"
                on_press:
                  then:
                    lvgl.page.next:
  pages:
    - id: page_light_switch
      widgets:
        - button:
            id: light_switch
            x: 10
            y: 35
            width: 150
            height: 110
            checkable: true
            styles: button_default
            on_click:
              switch.toggle: relay
            widgets:
              - label:
                  id: label_light_switch
                  align: CENTER
                  text: "\U000F1054"
#                  text: "\U000F0D35"     # lightbulb unlit
                  text_align: CENTER
                  text_font: mdi_icons
        - button:
            id: stair_switch_button
            x: 10
            y: 150
            width: 73
            height: 65
            checkable: true
            styles: button_default
            on_click:
              - logger.log: "Stairs button was pressed!"
              - homeassistant.service:
                  service: switch.toggle
                  data:
                    entity_id: "switch.1_stairs"
            widgets:
              - label:
                  text: "Stair"
                  align: center
                  text_color: "0xFFFFFF"
        - button:
            id: garage_switch_button
            x: 87
            y: 150
            width: 73
            height: 65
            checkable: true
            styles: button_default
            on_click:
              - logger.log: "Garage button was pressed!"
              - homeassistant.service:
                  service: switch.toggle
                  data:
                    entity_id: "switch.1_garage"
            widgets:
              - label:
                  text: "Garage"
                  align: center
                  text_color: "0xFFFFFF"
        - button:
            id: living_switch_button
            x: 10
            y: 220
            width: 73
            height: 65
            checkable: true
            styles: button_default
            on_click:
              - logger.log: "Living button was pressed!"
              - homeassistant.service:
                  service: switch.toggle
                  data:
                    entity_id: "switch.2_living_room"
            widgets:
              - label:
                  text: "Living"
                  align: center
                  text_color: "0xFFFFFF"
        - button:
            id: kitchen_switch_button
            x: 87
            y: 220
            width: 73
            height: 65
            checkable: true
            styles: button_default
            on_click:
              - logger.log: "Kitchen button was pressed!"
              - homeassistant.service:
                  service: switch.toggle
                  data:
                    entity_id: "switch.2_kitchen"
            widgets:
              - label:
                  text: "Kitchen"
                  align: center
                  text_color: "0xFFFFFF"

text_sensor:
  - platform: wifi_info
    ssid:
      name: ESP Connected SSID
    bssid:
      name: ESP Connected BSSID
    mac_address:
      name: ESP Mac Wifi Address
    scan_results:
      name: ESP Latest Scan Results
    dns_address:
      name: ESP DNS Address