Scenes around your QAs — the glue, tested offline

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.

One file, two parts

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.

Drive the trigger

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.

sourceTrigger — why did the scene start?

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

Glue, direction one: scenes calling QAs

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.

Glue, direction two: QAs triggering scenes

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.

Conditions the evaluator honors

Glue, direction three: scenes as schedules

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 scene tests itself

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.

What flua does not do yet

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.