limelight controls a smart light directly over your local network. It provides a web interface, a versioned JSON API, and a command line tool. It contacts no external service: every command is a UDP datagram sent from your machine to the device.
Contents
- What it does
- Supported devices
- How it works — the concepts, from networking up
- Install
- Adopting a device
- Running the service
- Command line
- Authentication
- Timed behaviour and its one limitation
- Running it permanently
- Documentation
What it does
| Web interface | Any browser, including a phone on the same network. Dark and light themes. |
| Android app | Standalone client in android/, talking to the lamp directly over miIO with no server running. Download the APK from Releases. |
| JSON API | Versioned at /api/v1, with an OpenAPI schema for generating clients. |
| Command line | Every function scriptable, suitable for cron or other automation. |
| Sunrise wake-up | Gradual brightness ramp over any duration. |
| Fade to sleep | Gradual dim to off. |
| Sleep timer | The device's own countdown, which continues if limelight stops. |
| Schedules | Recurring actions per weekday, with the next firings listed. |
| Device control | Power, brightness, eyecare mode, ambient light, four scenes, night light, fatigue reminder. |
Supported devices
| Model | Product | Verified on |
|---|---|---|
philips.light.sread1 |
Philips Eyecare Smart Lamp 2 | Firmware 1.2.8, MCU 0026, Wi-Fi 1.4.0 |
Hardware details for that model:
| Property | Value |
|---|---|
| Controller | ESP8266 |
| Radio | 2.4 GHz only; the chip has no 5 GHz support |
| Transport | UDP port 54321 |
| Open TCP ports | None. Ports 22, 23, 80, 443, 1883, 6668, 8080 and 8443 are all closed. |
| Encryption | AES-128-CBC, key derived from a 16-byte device token |
| Lights | Main LED panel and a separate ambient LED in the base |
| Brightness range | 1 to 100, both lights |
| Scenes | 3 fixed. Their effect is not exposed by the protocol, so they are reported as Scene 1 to 3. |
| Colour | None. Fixed white, no colour temperature control. |
| Native transitions | None. Every command takes effect immediately. |
| State notification | None. State must be polled. |
Adding another miIO light is a new file in limelight/drivers/. See
docs/DEVICES.md.
How it works
The concepts, in the order they matter. Byte-level detail lives in docs/PROTOCOL.md; this is the orientation.
What miIO is
miIO is the local control protocol spoken by devices in Xiaomi's smart home ecosystem, its name a contraction of Mi and IO. Xiaomi has never published a specification, so everything known about it publicly comes from reverse engineering.
Two properties explain things that otherwise look odd here.
The brand on the box does not determine the protocol. Xiaomi runs an ecosystem
programme in which partner manufacturers build hardware that registers with the Mi Home
application, so a Philips-branded lamp is addressed exactly like a Xiaomi air purifier.
Each device carries a model identifier of the form vendor.category.model:
philips.light.sread1
| | |
| | +-- model, this specific product
| +--------- category, a light
+---------------- vendor within the ecosystem programme
That identifier, not the brand, selects a driver.
There are two generations. Legacy miIO addresses properties by string name through
get_prop; MIoT-Spec-V2 addresses them numerically through get_properties with a service
and property id. Both use the same transport, so the difference is purely a driver concern.
This lamp is a legacy device.
A cloud path over HTTPS also exists for devices bound to a Xiaomi account. limelight never
uses it, and provisions with uid=0 specifically to avoid creating that binding.
Access points, and the two modes a device can be in
An access point is the radio other devices associate with to form a Wi-Fi network. Your router runs one; its network name is the SSID you pick from a list.
Infrastructure mode is the normal state: the device joins your router's access point and
DHCP gives it an address on your subnet, such as 192.168.1.42.
SoftAP mode is the setup state. A device with no stored Wi-Fi credentials cannot join
anything, so it runs an access point of its own and waits for you to connect to it. The
SSID encodes what it is, and _miap marks it as a miIO setup network:
philips-light-sread1_miapXXXX
Two consequences: your machine loses internet while joined to it, and this device's setup
network advertises no default gateway, so code that derives the device address from the
routing table finds nothing. It answers on 192.168.4.1. That detail defeated the first
adoption attempt and is why docs/ADOPTION.md exists.
Addressing: the device id matters more than the IP
Addresses come from DHCP and move. Every miIO device also has a device id, a 32-bit integer fixed for the life of the hardware. limelight stores both, and when commands start failing it re-runs discovery, matches on the device id, and updates the address by itself.
Transport: why UDP means retries
Commands are UDP datagrams to port 54321. UDP is connectionless, with no delivery acknowledgement, which has one consequence worth internalising:
A lost datagram is indistinguishable from a dead device.
Every command is therefore retried with increasing backoff before being called a failure. A rediscovery warning in the log usually means one datagram was dropped, not that anything is broken.
Authentication: the token
Access is controlled by a single token: 16 bytes, written as 32 hexadecimal characters. There is no username, no password, and no per-command permission. Possession of the token is complete control of the device.
The token is never transmitted. It seeds the encryption, with the key derived as
md5(token) and the IV as md5(key + token), and the payload encrypted with AES-128-CBC.
A device that cannot decrypt a request simply does not answer, so a wrong token presents
as an unreachable device rather than as an authentication error.
The handshake, and how a token is obtained
Before any encrypted exchange, a client sends a 32-byte hello with no payload. No credential is involved, so any device on the subnet answers with its device id, and firmware that discloses its token includes that too.
This is both the discovery mechanism and how a token is obtained without vendor software. Whether it works depends on binding: a device that has never been registered to a Xiaomi account discloses its token, and binding one regenerates it.
Query and response
Payloads are JSON-RPC. A read asks for several properties at once and gets them back in order:
// query
{"id": 1, "method": "get_prop", "params": ["power", "bright", "eyecare"]}
// response
{"result": ["on", 70, "off"], "id": 1}
A write names a method and its arguments, and answers {"result": ["ok"]}.
Reading everything in one datagram is not a nicety. The device is a single-threaded microcontroller on a connectionless transport, and one request per property drops datagrams.
The full property list and command surface for this lamp is in docs/PROTOCOL.md, including one coupling a client must respect: enabling eyecare hands brightness to the mode, and setting brightness cancels eyecare. The two cannot be held at once.
Capabilities: how clients avoid hard-coding a feature set
Each driver declares what its device can do, and both the API and the Android app render controls from that list rather than from assumptions:
$ curl -s http://localhost:8765/api/v1/device | jq '.capabilities'
["ambient","ambient_brightness","brightness","eyecare",
"night_light","power","reminder","scenes","sleep_timer"]
Three things follow. Clients build their interface from the list. Unsupported operations
return a clean HTTP 400 rather than a protocol error. And in GET /api/v1/state, only on
is guaranteed, so an absent key means the device has no such feature.
Capability strings are public API, and clients must ignore values they do not recognise, so an older client keeps working against a newer device.
Putting it together
sequenceDiagram
participant C as Client<br/>(browser, phone, curl)
participant S as limelight<br/>HTTP service
participant D as Driver<br/>+ transport
participant L as Lamp<br/>UDP 54321
C->>S: GET /api/v1/device
S->>C: capabilities, scenes, brightness range
Note over C: build the interface from capabilities
C->>S: GET /api/v1/state
S->>D: state()
D->>L: get_prop [power, bright, ...] (encrypted)
L->>D: [on, 70, ...]
D->>S: LightState
S->>C: state + schedules + ramp + next runs
C->>S: POST /api/v1/brightness {"level": 45}
S->>D: set_brightness(45)
D->>L: set_bright [45] (encrypted)
L->>D: ["ok"]
S->>C: {"ok": true, "result": ["ok"]}
The Android app removes the middle two participants: it speaks miIO to the lamp directly.
Further reading: docs/PROTOCOL.md for the wire format, docs/ARCHITECTURE.md for how the code is layered, and docs/API.md for the HTTP contract.
Install
Python 3.11 or newer.
From PyPI, once published:
pip install limelight-miio
The distribution is limelight-miio because PyPI does not accept the bare name. Everything
else stays limelight: import limelight, and the limelight and limelight-server
commands.
From a checkout, for development or to run the newest code:
git clone https://github.com/smartwhale8/limelight.git
cd limelight
python -m venv .venv && source .venv/bin/activate
pip install -e ".[server]" -c constraints.txt
constraints.txt pins the exact versions this project is built and tested against, so the
install is reproducible. pyproject.toml declares looser ranges, so limelight can still be
installed alongside other packages that need different versions.
Adopting a device
Adoption means recording a device's address and token so limelight can command it.
If the device is already on your network and discloses its token, this is one step:
limelight discover --subnet 192.168.1.
limelight adopt --auto --name "Desk lamp"
discover reports which devices disclose a token:
192.168.1.42 device_id=12345678 (token disclosed)
If it reports token withheld, or the device is unprovisioned and broadcasting its own
SoftAP, follow docs/ADOPTION.md, which covers recovering the token over
the setup access point and handing the device your Wi-Fi credentials. Then:
limelight adopt --ip 192.168.1.42 --token 0123456789abcdef0123456789abcdef
Settings are written to ~/.config/limelight/config.json at mode 0600, because that file
holds the token.
Running the service
limelight serve
Open http://localhost:8765, or http://<your-machine-ip>:8765 from a phone on the same
network. /docs serves interactive API documentation and /openapi.json the schema.
Command line
limelight status # current device state
limelight on --brightness 60
limelight off
limelight brightness 35
limelight scene 3 # the device accepts 1, 2 or 3
limelight eyecare on
limelight ambient on --level 50
limelight timer 45 # device countdown; survives limelight exiting
limelight sunrise --minutes 20 --target 100
limelight fade --minutes 30
limelight schedules
limelight capabilities # what this device supports
limelight info # raw miIO.info
limelight models # drivers available
Authentication
limelight requires no authentication by default. Anything that can reach the port can control the device.
To require a key:
export LIMELIGHT_API_KEY="$(openssl rand -hex 24)"
limelight serve
Clients then send Authorization: Bearer <key>, or X-API-Key: <key>. The web interface
prompts once and stores it. GET /api/v1/health stays unauthenticated so a client can
identify a service before pairing, and its auth_required field reports whether a key is
needed.
The key is a shared secret over plain HTTP. It identifies a client on a trusted network. Do not forward the port to the internet; reach it through a VPN into your network instead.
A separate consideration applies to the device itself: this firmware discloses its token to any unauthenticated request on the local network, so the device's real perimeter is your Wi-Fi password. See SECURITY.md.
Timed behaviour and its one limitation
The device firmware implements exactly one timed feature: delay_off, a hard cut-off after
N minutes. Gradual wake-up and gradual fade-out do not exist in the hardware. limelight
produces them by stepping set_bright every five seconds, which has a consequence:
- A
sunriseorfade_offramp progresses only while limelight is running. If the host sleeps, the ramp stops where it was. - A
timerschedule uses the device's own countdown and survives limelight exiting, a reboot, or the host sleeping.
The API and the interface both mark which is which, through the service_driven field. For
a wake-up you depend on, run limelight on a machine that stays awake.
| Schedule kind | Behaviour | Survives limelight stopping |
|---|---|---|
sunrise |
On at 1%, ramping to the target over the duration | No |
fade_off |
Ramp down to 1% over the duration, then off | No |
on |
On, applying brightness, scene and ambient | Yes, it is instant |
off |
Off | Yes, it is instant |
timer |
Set the device countdown | Yes, the device tracks it |
Running it permanently
So that schedules fire without the service being started by hand.
macOS, using launchd:
./packaging/launchd/install.sh
Linux, using systemd:
./packaging/systemd/install.sh
Each script prints what it will do before doing it, and each has a matching
uninstall.sh.
Documentation
| Document | Contents |
|---|---|
| docs/PROTOCOL.md | miIO on the wire, packet layout, crypto, full command set |
| docs/API.md | The HTTP contract, with captured responses |
| docs/ADOPTION.md | Token recovery and provisioning, step by step |
| docs/ARCHITECTURE.md | Layers, threading, state, and the reasoning |
| docs/DEVELOPMENT.md | Setup, testing, debugging, releasing |
| docs/DEVICES.md | Adding support for another device |
| docs/TROUBLESHOOTING.md | Symptoms, causes, fixes |
| docs/ROADMAP.md | Planned work |
| android/README.md | The Android client: build, design, and how its codec was verified |
Contributing
Issues and pull requests are welcome, particularly drivers for other miIO lights. See CONTRIBUTING.md. The test suite runs without hardware, so a contribution can be verified before it touches a device.
Acknowledgements
Built on python-miio, which implements the miIO transport and cryptography.
License
MIT.
limelight is not affiliated with, endorsed by, or connected to Xiaomi, Signify, or Philips. Product names identify the hardware the software controls.
Metadata
Release files for limelight-miio 2.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| limelight_miio-2.1.0.tar.gz | 112.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| limelight_miio-2.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 158.5 kB
Release files / limelight_miio-2.1.0.tar.gz
| Download URL | limelight_miio-2.1.0.tar.gz |
|---|---|
| Size | 112.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d0ff676365437de910a151ab60f203bceaad3b7e74824c309552fbdfa4936556
|
|
BLAKE2b-256 checksum How to use checksums |
aaf70b0c69b12affdf3589199e0c23f465ea1c84b1d16e78b94e0197fde7d7da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency logRelease files / limelight_miio-2.1.0-py3-none-any.whl
| Download URL | limelight_miio-2.1.0-py3-none-any.whl |
|---|---|
| Size | 46.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c9456bbcc0ae79379c602cd848073de13d8d45313134d5a441442bb0c8a3b285
|
|
BLAKE2b-256 checksum How to use checksums |
0edfbba569a902f4f8381d8abc248c692d10de1cc84a9bcbed4ce4eb109f8044
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency log