Luke Roberts ESPHome integration

Luke Roberts Lamp via ESPHome – no MQTT, no bridge, no proxy

As already discussed in a few other threads:

…there is no first-class integration for Luke Roberts lamps in Home Assistant, and the existing workarounds all rely on an extra moving part — a Python BLE script, an MQTT bridge, or the HA Bluetooth Proxy feeding a custom component.

I wanted to do it with just an ESP32 as the controller, directly talking to the lamp over BLE, exposed to Home Assistant via the native ESPHome API. No MQTT broker. No Bluetooth proxy. No Python integration. One YAML file.

:backhand_index_pointing_right: github.com/3DJupp/lukeroberts-esphome-lampcontrol


Approach

The lamp implements a clean, documented Lamp Control API v1.3 over a custom GATT service (44092840-0567-11E6-B862-0002A5D5C51B). Every command is a short byte sequence written to the External API characteristic; every response comes back as a notification on the same characteristic.

ESPHome already has everything we need: ble_client, ble_client.ble_write as an action, notify-capable text_sensor: platform: ble_client, scripts, intervals, globals. So the whole thing fits naturally into a single config with no external components.


What works

  • Scene selection – up to 16 scenes (ids 0–15), plus Off and Default (power-up scene), addressable via individual buttons or a single select dropdown entity; brighter/dimmer stepping also supported
  • Automatic scene-name discovery – on every connect the gateway walks the lamp’s scene list via the Query Scene opcode and publishes each name as a HA text sensor, plus a combined "0: Off | 1: Reading | 2: Movie | …" overview string. A Refresh Scenes button re-walks on demand.
  • Brightness – absolute (percent) and relative (multiplicative ±20 %)
  • Color temperature – 2700 K – 4000 K
  • Immediate Light – transient brightness + kelvin overrides
  • Ping keepalive – fires every 6 s for 30 s after the last user command, so follow-up commands land in < 100 ms without re-establishing the link; once idle the lamp is free to sleep
  • Live response decoding – every write’s status byte is published to a diagnostic text sensor (00 OK, 84 Invalid Id, FC Forbidden, …)
  • Device info – manufacturer, model, serial, firmware/hardware/software revision auto-populated on connect from service 0x180A

Dashboard

The repo includes two ready-to-paste Lovelace snippets under dashboard/:

  • lamp-card.yaml – everyday control panel. Switches automatically between three states via conditional cards: mains off → only a status glance row; mains on but BLE not yet reachable → waiting notice; fully reachable → scene picker, brightness & color-temperature controls.
  • diagnostics-card.yaml – admin card with Ping / Restart / Refresh Scenes / last API response / device info, gated by visibility so it only shows for a specific HA user.

The main card uses only core HA cards plus optionally custom:mushroom-select-card (HACS) for the scene dropdown.


Command quick-reference

Action Bytes
Ping (v2) A0 02 00
Lamp Off A0 02 05 00
Default / Power-up scene A0 02 05 FF
Scene N A0 02 05 0N
Brightness P % A0 01 03 PP
Color Temperature K A0 01 04 KK KK
Next brighter / dimmer scene A0 02 06 01 / FF
Relative brightness × P % A0 02 08 PP
Query Scene id N A0 01 01 NN

Getting started

  1. Flash the YAML to any ESP32 (tested on an ESP32-S3-WROOM-1).
  2. Put your lamp’s BLE MAC into the lr01_mac substitution.
  3. Adopt in Home Assistant — the device appears automatically via mDNS.

All credentials live in secrets.yaml; a secrets.yaml.example is included.


Caveats

  • Lamps with a security code set via the Luke Roberts app will reject commands with FC Forbidden. Clear the code in the app first.
  • There is no “currently active scene” query in the protocol, so the select entity tracks the last command sent rather than the lamp’s actual state. Fine for automations and buttons; a future extension could persist this via restore_value.
  • Firmware < 1.0.7 may not support the full command set — the spec notes which firmware version introduced each opcode.
  • The lamp drops the BLE link after ~8 s of inactivity. auto_connect: true + the ping keepalive handle reconnection transparently, with a one-off ~1–2 s latency if the link happened to be down.

Optional: pretty-named scene dropdown on the HA side

The select entity in ESPHome uses a static options list (bare ids 0–15 + Default), because HA only fetches the options list once on connect. The discovered scene names are published separately as text sensors and as the Scenes Overview string right next to the dropdown.

If you want a dropdown that actually reads "1: Welcome!", "2: Highlights", … drop the package under home-assistant/ into your HA config — it creates an input_select.lr_scene helper and two automations that keep the option list in sync and translate a selection into the matching button press on the gateway. Details in home-assistant/README.md.


Feedback welcome

If you have a Luvo or Model F, try it and let me know what breaks. I’ve only been able to test on a single lamp so far. In particular:

  • Does scene enumeration match what the Luke Roberts app shows?
  • Does the Immediate Light uplight sub-packet (XX[0] = 1, HSB) work on older firmwares?
  • Anyone running a security-code setup they’d like to pair through ESPHome?

PRs happily accepted. :slightly_smiling_face: