An offline DALI lighting control simulator for testing AI agents before you point them at a real site.
You get one or more simulated DALI lines full of control gear and sensors. An agent drives them through MCP, REST or plain Python, and a dashboard shows what is actually happening. It works at command level: no bit timing, but the IEC 62386 behaviour is all there. That covers short/group/broadcast addressing, DAPC and fades, the 16 scenes and 16 groups, min/max/power-on/system-failure levels, the status byte and queries, DT8 colour (Tc, RGBWAF, xy), DT6 dimming curves, random-address commissioning, and part 103 sensors and push buttons. The trace still shows real forward frames (FF 05 BC RECALL_MAX_LEVEL), so it reads like a bus monitor.
uv sync
uv run dali-sim serve officeThen open http://127.0.0.1:8765 for the dashboard. REST lives under /api (OpenAPI docs at /docs) and MCP over streamable HTTP is at /mcp.
For an MCP client that wants stdio (Claude Desktop, Claude Code, etc.):
{
"mcpServers": {
"dali": {
"command": "uv",
"args": ["--directory", "/path/to/dali-sim", "run", "dali-sim", "mcp", "office", "--agent-mode"]
}
}
}The stdio server still runs the dashboard on :8765 so you can watch the agent work (--no-dashboard turns it off).
Useful flags on serve and mcp:
| flag | what it does |
|---|---|
--virtual-clock |
time only moves when you call sim_advance_time / POST /api/sim/advance, which makes runs repeatable |
--agent-mode |
hides the sim_* MCP tools so the agent under test can't peek at ground truth or inject its own faults |
--seed N |
RNG seed for random addresses and fault rolls |
--trace |
prints bus traffic to stderr |
Other commands: dali-sim state (table of a running sim), dali-sim trace -f, dali-sim events -f, dali-sim validate my-site.yaml, dali-sim sites.
Three presets ship with the package (src/dali_sim/sites/):
- office: one line with tunable white open plan, an RGBW meeting room cove, a corridor on a PIR, a wall switch and a daylight sensor. The built-in controller handles the PIRs and the switch.
- retail: two lines (sales floor and back of house) with scenes for opening, trading, closing, cleaning and night. There are no controller rules, so the agent runs the shop.
- warehouse: 18 high bays fresh out of the box with no short addresses. The agent has to commission, group and scene them.
Write your own by copying one. The format looks like this:
name: my-site
seed: 1
buses:
- id: line1
groups: {0: lobby}
scenes: {0: day, 15: night}
gear:
- name: lobby_dl_{n} # count expands {n} and bumps the address
count: 4
address: 0 # leave out for unaddressed gear
type: tc # led | tc | rgbwaf | xy | colour
room: Lobby
groups: [lobby]
fade_time: 4
scenes: {day: 254, night: {level: 80, kelvin: 2700}}
inputs:
- name: lobby_pir
address: 0
instances:
- {type: occupancy, hold_time: 300}
- {type: light, daylight: 400, sees: {lobby_dl_1: 120}}
controller:
enabled: true
rules:
- when: {input: lobby_pir, event: occupied}
do: {target: lobby, scene: day}Quote "on" and "off" in YAML, otherwise they turn into booleans.
Every control call takes a target, which can be:
- a gear name
- a group name
room:<name>all- a raw address:
A0-A63,G0-G15,BCorBCU
On multi-bus sites, prefix the target with the bus, e.g. line2:G0.
Gateway tools behave like a real gateway: set_level, recall_scene, set_colour, store_scene, set_group, configure, query, read_status, send_command, send_raw_frame, scan_bus, commission_bus, read_sensor, read_events, read_trace and friends. Commands are fire-and-forget like on a real bus, so if a frame is dropped the agent only finds out by querying.
Sim tools (sim_*, also exposed under /api/sim/*) are the world side:
- people walking past sensors and pressing buttons
- daylight
- the clock
- resetting the site
- faults:
lamp_failure,gear_failure,no_response,input_no_responsebus_power_down,mains_power_cycledrop_rate,garble_rate,latency_ms
A typical test harness drives the scenario over REST and gives the agent MCP in --agent-mode.
from dali_sim import load_site
site = load_site("office", virtual_clock=True)
site.set_level("open_office", percent=60)
site.advance(3)
print(site.read_status("office_dl_1"))uv run pytestThese are left out:
- bit-level timing and the 24-bit part 103 frames (input devices are event-level)
- memory banks beyond bank 0
- writable memory
- DT8 primary-N channels
- the multi-master arbitration