This is the "big picture" version of ARCHITECTURE.md. It is meant for QuickApp developers who use plua every day but don't particularly care about the Python internals — you just want a mental model of what's happening when you press Run.
If you ever need the deep-dive (every module, every queue, every class), the full ARCHITECTURE.md is one click away.
Plua is two things bolted together:
- A Lua interpreter that runs on your laptop. Fibaro's Home Center 3 (HC3) is itself a Lua machine, and plua runs the same Lua 5.4 dialect.
- A fake HC3 written in Lua. A few thousand lines of Lua code
pretend to be a Home Center 3 — there is a device list, a
fibaro.*API, a UI engine, scenes, global variables, the works. Your QuickApp talks to this fake HC3 exactly the way it would talk to a real one.
The Lua interpreter is hosted by Python (using a library called Lupa), because Python gives us easy access to networking, an HTTP server, and a window manager — all things that a plain Lua interpreter doesn't have built-in.
So, very roughly:
┌─────────────────────────────────────────────────┐
│ plua process (one program) │
│ │
│ ┌────────────────────┐ ┌────────────────┐ │
│ │ Your QuickApp │ │ Browser │ │
│ │ (Lua) │ │ (UI windows) │ │
│ └─────────┬──────────┘ └────────┬───────┘ │
│ │ fibaro.* │ HTTP/WS │
│ ┌─────────▼──────────┐ ┌────────▼───────┐ │
│ │ Fake HC3 (Lua) │◄──►│ Web server │ │
│ │ emulator + API │ │ (FastAPI) │ │
│ └─────────┬──────────┘ └────────┬───────┘ │
│ │ _PY.* │ │
│ ┌─────────▼─────────────────────────▼───────┐ │
│ │ Python core: timers, HTTP/TCP/UDP/WS/MQTT│ │
│ │ network clients, file I/O │ │
│ └───────────────────────────┬───────────────┘ │
└───────────────────────────────┼─────────────────┘
│
┌────────▼────────┐
│ Real HC3 or │
│ the wider net │
└─────────────────┘
Plain Lua. The same code you would upload to a real HC3. It calls
fibaro.call(...), self:updateProperty(...), setTimeout(...) and so
on, blissfully unaware that it's running on your laptop.
Also pure Lua. A singleton object called Emu is the heart of it:
- It keeps a table of every "device" that exists in this fake HC3 (your QA, plus any child devices, plus a couple of housekeeping entries).
- It implements the
fibaro.*functions and the/api/...REST routes. - It compiles your
--%%u:UI headers into something the browser can draw. - When you call
fibaro.call(123, "turnOn"),Emudecides whether to handle it locally, forward it to a real HC3, or look it up in an in-memory store — that decision is what we call modes (see below).
The user-facing class QuickApp lives here too. When plua starts your
script, it parses the --%% headers, builds a synthetic device record,
registers it with Emu, and calls your onInit().
A small number of Python modules that exist mainly to give the Lua side super-powers it doesn't have on its own:
engine.py— owns the Lua VM and the asyncio event loop. Think of this as "the heartbeat".timers.py— when you callsetTimeout(fn, 1000)in Lua, this is what schedules it.lua_bindings.py— the bridge. Anything Python wants to expose to Lua is decorated with@export_to_luaand shows up in Lua as_PY.somefunction(). (You almost never call_PYdirectly — Lua wrappers likenet.HTTPClientdo that for you.)fastapi_process.py— runs the small web server that powers the UI windows and the REST API.window_manager.py— opens browser windows for QA UIs and remembers their position.
Roughly one Python file per protocol: HTTP, TCP, UDP, WebSocket, MQTT,
plus filesystem access. These are the things net.HTTPClient,
net.TCPSocket, etc. quietly call into. They don't know anything about
Fibaro — they just do networking and report results back to Lua via a
callback.
A simplified play-by-play of ./run.sh --fibaro myqa.lua:
- Python starts up. It reads your command-line flags, makes sure the API port is free, and starts the small FastAPI web server in the background.
- The Lua VM is created and a few hundred Python helpers are
exposed to it under the global
_PYtable. init.luaruns. It patches some things (UTF-8, timers,print) and waits to be told what file to load.fibaro.luaruns (because of--fibaro). This brings the fake HC3 to life: it createsEmu, sets up the API router, and tellsinit.lua"actually, let me load the user's file."- Your file is loaded. plua reads the
--%%headers, builds a device record, and creates an instance of yourQuickAppclass.QuickApp:onInit()is called. - The event loop runs. Timers fire, HTTP responses arrive, UI
clicks come in from the browser, the QA reacts. This continues until
--run-forsays it's time to quit (default: at least 1 second, then exit when nothing is pending).
When your QA does net.HTTPClient():request(url, ...):
QA (Lua)
│ net.HTTPClient:request(url, callback)
▼
net.lua wrapper
│ _PY.call_http(url, callback_id) ← callback registered
▼
pylib/http_client.py
│ asyncio task: aiohttp does the request
▼ (network)
The actual HTTP server
│
▼ response comes back, possibly on another thread
engine.post_callback_from_thread(id, result)
│ result lands on a queue
▼ next event-loop tick
Lua callback runs with the response
The same pattern works for TCP, UDP, WebSocket, MQTT, timers, and even "a HTTP request hit our FastAPI server, please handle it in Lua". It is always: register a callback ID in Lua → do work in Python → drop the result on a queue → Lua gets called back. This is what lets a single Lua thread coexist with everything async without ever blocking.
A QA always runs in one of three modes (set by --%%offline:true,
--%%proxy:true, or neither):
| Mode | What fibaro.call(...) actually hits |
|---|---|
| Online (default) | The fake HC3 first, and anything it can't handle is forwarded to your real HC3 over HTTP. You need credentials in .env. |
| Offline | Only the fake HC3, plus an in-memory store seeded with some realistic dummy devices. No network at all. Great for CI and trains. |
| Proxy | Your QA runs locally and a tiny stub QA is installed on the real HC3. UI clicks and HC3 events tunnel back to plua over a WebSocket, so you can iterate locally while still reacting to the real device. |
When a QA has --%%desktop:true, plua opens a browser window pointed at
its built-in web server. The page (quickapp_ui.html) draws your
buttons, sliders, and labels by fetching /plua/quickApp/<id>/info.
When you click a button:
- The browser POSTs to
/api/plugins/callUIEvent. - FastAPI hands the request to the engine.
- The engine looks up the QA, calls the right handler method.
- If the handler calls
self:updateView(...), the engine pushes the change back to the browser over a WebSocket so the UI updates live.
You don't have to think about any of this — just write onReleased
handlers and call updateView.
src/
├── plua/ ← Python: engine, web server, bridge, window manager
├── pylib/ ← Python: HTTP/TCP/UDP/WS/MQTT/filesystem primitives
└── lua/
├── init.lua, json.lua, net.lua, ... ← runtime + std libs
└── fibaro/ ← the fake HC3 (Emu, QuickApp, UI compiler, modes, ...)
Things you'll touch as a QA developer: your own .lua file, the
HC3 credentials in .env, and sometimes a header in .project.
Things you'll never need to touch (but now know exist): Python core, the FastAPI server, the queue plumbing.
- Want a concrete starter QA?
plua --init-qascaffolds a project. - Want to see every API your QA can call? Ask Copilot for
/quickapp-api. - Want common patterns (timers, polling, child devices)? Ask for
/quickapp-patterns. - Want the deep architecture? Read ARCHITECTURE.md.
- Stuck?
/plua-troubleshootingand/quickapp-troubleshootingcover the common pitfalls.