HC3 scenes are the easiest automation there is: draw a block scene in the controller's editor and it just runs. Under the hood the editor compiles the blocks to a Lua scene — the same format you can also write by hand. flua isn't a scene editor, and this post isn't pushing you to write more scenes: it runs the compiled format because scenes are glue. A scene watches the house and calls your QuickApps; a QA under development gets driven by scenes; a scene's schedule becomes your QA's schedule. Scenes are also where the subtle bugs live — triggers fire on changes only, and a scene that silently doesn't run is hard to diagnose on the controller. flua makes that glue deterministic and testable offline. Here's the file shape, and the three directions of the glue.
A block scene's "Convert to Lua" produces exactly this shape:
declarations above a marker line, actions below it. Save it as
bedtime.lua:
--%%name:Bedtime
--%%scene:true
COND = {
operator = "any",
conditions = {
{ type = "device", id = 10, property = "value",
operator = "==", value = true, isTrigger = true },
}
}
-- ===== SCENE ACTIONS =====
print("SCENE_RAN", _sceneId, sourceTrigger.type, sourceTrigger.id)
fibaro.call(10, "turnOff")
print("LAMP_AFTER", api.get("/devices/10").properties.value)
Boot parses the declarations only — the actions never run at boot,
like the HC3. Every scene start runs the actions fresh, with
sourceTrigger and _sceneId in scope. The scene
also answers GET /scenes, so the REST surface knows it
exists.
The scene watches device 10 — so the sim needs a device 10. The seed
file pre-populates the simulated house before anything runs (the same
format --tool createDB writes): here, one lamp, off. Then a
one-line driver QA flips it — and the scene's trigger fires.
house.json — the simulated house, seeded:
{"devices": [{"id": 10, "name": "Hall lamp",
"type": "com.fibaro.binarySwitch",
"properties": {"value": false}}]}
driver.lua — flips the lamp one second after boot:
--%%name:Driver
function QuickApp:onInit()
setTimeout(function()
api.put("/devices/10", { properties = { value = true } })
end, 1000)
end
flua --api local bedtime.lua driver.lua --seed house.json
[Bedtime5000]: SCENE_RAN 5000 device 10 [Bedtime5000]: LAMP_AFTER false -- the fibaro.call took effect
One trap worth knowing immediately: triggers fire on changes. Flip a switch to the value it already has and nothing happens — on the HC3 and in flua alike.
The actions branch on sourceTrigger.type:
device (with .id/.property/
.value), date, global-variable, or
manual. Manual means a person pressed Run — or another QA
called it:
fibaro.scene("execute", { 5000 }) -- runs the actions now: type == "manual"
-- or plain REST: POST /scenes/5000/execute
[Bedtime5000]: SCENE_RAN 5000 manual
The rule of thumb on the HC3: QuickApps are the drivers, scenes chain actions by calling QA methods. That is exactly what you want to test when the QA is yours and still changing — the scene stays on the controller, its target method gets exercised here. flua assigns QA ids from 5000 in load order, so put the QA first on the command line and the scene knows its id. Take the driver from above, give it the method under development, and let the scene call it:
driver.lua — the method under development:
function QuickApp:turnOn() print("QA_TURNON") end
scene.lua — the actions, when the lamp flips:
--%%name:Scene
--%%scene:true
COND = {
operator = "any",
conditions = {
{ type = "device", id = 10, property = "value",
operator = "==", value = true, isTrigger = true },
}
}
-- ===== SCENE ACTIONS =====
print("SCENE_RAN", _sceneId, sourceTrigger.type, sourceTrigger.id)
fibaro.call(5000, "turnOn")
flua --api local driver.lua scene.lua --seed house.json
[Scene5001]: SCENE_RAN 5001 device 10
[Driver5000]: QA_TURNON -- the scene reached the QA's action
On the real HC3 the scene references the QA's real id — the code is the same, only the number changes.
The reverse also works: a QA's own property update is a
DevicePropertyUpdatedEvent on the refreshStates feed, so a
scene watching that QA fires — the QA becomes the scene's trigger, no
controller involved:
scene.lua:
--%%name:Watcher
--%%scene:true
COND = {
operator = "any",
conditions = {
{ type = "device", id = 5000, property = "value",
operator = "==", value = true, isTrigger = true },
}
}
-- ===== SCENE ACTIONS =====
print("SCENE_RAN_QA", _sceneId, sourceTrigger.type, sourceTrigger.id)
driver.lua:
--%%name:Driver2
function QuickApp:onInit()
setTimeout(function() self:updateProperty("value", true) end, 1000)
end
flua --api local driver.lua scene.lua --seed house.json
[Watcher5001]: SCENE_RAN_QA 5001 device 5000
Global variables work the same way — a
global-variable trigger leaf and
fibaro.setGlobalVariable on the QA side.
isTrigger = true) start a
check; filter leaves gate the run. operator is
"all" or "any", groups nest.anyValue triggers on any change;
match>/match>=/match</
match<= are filters-only (a trigger flag on them warns and
is ignored).duration = 60 defers the run and re-checks the whole
tree at now + 60 — a change that breaks the condition
cancels the pending run.cron is
{minute, hour, dom, month, dow, year} with dow 1 = Monday and
lists like "1,3" allowed; matchInterval fires on
the grid, date + k*interval; sunrise/
sunset take a minute offset (negative = before).A scene with a date trigger is the HC3's built-in scheduler — the easiest way to make a QA do something at 23:00, at sunrise, or every ten minutes. The same cron scene calling the QA under development, with the virtual clock doing the waiting:
--%%name:Scheduler
--%%scene:true
COND = {
operator = "any",
conditions = {
{ type = "date", property = "cron", operator = "match",
value = {"0", "23", "*", "*", "*", "*"}, isTrigger = true },
}
}
-- ===== SCENE ACTIONS =====
print("SCENE_RAN", _sceneId, sourceTrigger.type)
fibaro.call(5000, "turnOn")
flua --api local driver.lua cron.lua --seed house.json \ --start "2026/10/19 22:59:30" --speed 120 --max-hours 1 [Scheduler5001]: SCENE_RAN 5001 date -- at virtual 23:00:00 [Driver5000]: QA_TURNON
The daily cron re-arms a day ahead; --max-hours caps the
horizon so the demo ends instead of waiting for tomorrow.
The self-asserting pattern from the test post applies here unchanged: a driver QA flips devices, the scene's actions leave traces (a global variable, a property), and the driver asserts the effect and exits with the failure count. The bundled example does exactly that — it loads a scene from an inline string, triggers it by flipping a device, then executes it manually, checking the result each time:
flua --api local examples/scenes.lua PASS: scene in /scenes PASS: device trigger ran the scene PASS: manual execute ran the scene failures: 0 -- exit 0
Run it under flua test or in CI and a scene bug is a red
test, not a surprise on the controller.
Honest edges: getSceneVariable/setSceneVariable
are still stubs (use global variables for now) and the "Allow restarting
a running scene" option is not implemented. Weather, alarm and profile
conditions evaluate against simulated state offline; in online mode they
read the real HC3 — the weather record, the alarm partitions and the
profiles come from the controller. And flua is no scene editor: draw the
block scenes on the HC3, convert, and run the compiled Lua here — flua's
job is making the glue testable, not replacing the editor.
Next time: endpoint packages — wrap a simulated endpoint in a QA and distribute it as a .fqa, emulation like any other QA.