quickapp-api
Skill1 starsChanged 15 days agoMITView on GitHub
Full HC3 QuickApp Lua API reference: fibaro.* functions, QuickApp methods (self:updateProperty, self:updateView, self:getVariable, etc.), plugin.* API, api.* REST calls, net.HTTPClient/TCPSocket, timers (setTimeout/setInterval), JSON, and standard Lua libraries available in QA context. USE FOR: writing QuickApp code, looking up specific method signatures, understanding what functions are available, fixing "attempt to call nil" errors on QA methods.
In this repository18 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .github/copilot-instructions.mdCopilot instructions
- code-tourSkill
- flua-setupSkill
- flua-troubleshootingSkill
- hc3-lua-scenesSkill
- hc3-rest-apiSkill
- hc3-write-safetySkill
- lua-basicsSkill
- network-device-testSkill
- plua-migrationSkill
- quickapp-patternsSkill
- quickapp-testSkill
- quickapp-troubleshootingSkill
- quickapp-typesSkill
- quickapp-uiSkill
- qwikchildSkill
- skill-creatorSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 1 is distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 14
Every QuickApp starts with onInit().
- Distinctive: line 34
All logging methods accept multiple arguments separated by commas.
- Very specific: line 239
silently: when the server closes (HC3 behavior), and a mid-stream read timeout is a transport failure → error.
- Very specific: line 323
When building an FQA structure to POST to /api/quickApp/, several nested fields are array-typed on HC3.
- Very specific: line 369
All QuickApp methods are publicly callable via fibaro.call(id, "method") — keep private helpers as local functions outside the class
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
QuickApp Lua API Reference
Complete reference for the Fibaro HC3 QuickApp Lua programming environment. Use this skill when writing QuickApp code with flua.
QuickApp Lifecycle
Every QuickApp starts with onInit(). The global quickApp variable is set only after onInit() returns.
function QuickApp:onInit()
self:debug("Started, id =", self.id, "name =", self.name)
-- initialize timers, load variables, etc.
endQuickApp Methods (self:...)
Logging
self:debug(...) -- DEBUG level — shown in HC3 debug console
self:trace(...) -- TRACE level
self:warning(...) -- WARNING level
self:error(...) -- ERROR levelAll logging methods accept multiple arguments separated by commas; they are concatenated with spaces.
Device Properties
self:updateProperty("value", true) -- persist and trigger events
self:updateProperty("log", "status msg") -- update log field
-- self.properties.value = true ← DO NOT do this, no event generatedCommon property names: value, dead, log, userDescription.
QuickApp Variables
local v = self:getVariable("myKey") -- returns "" if not found (not nil!)
self:setVariable("myKey", "myValue") -- triggers DevicePropertyUpdatedEvent
-- Always check: if v ~= "" then ...UI Updates
self:updateView("labelId", "text", "Hello world")
self:updateView("sliderId", "value", "75") -- value must be a STRING
self:updateView("btnId", "text", "Click me")
self:updateView("switchId", "value", "true") -- "true" or "false" as stringChildren
Advanced child management (attach-or-create by UID, per-child classes and UIs): the qwikchild library —
--%%file:$qwikchild.lua,qwikchild, see the qwikchild skill.
-- Create a child device
local child = self:createChildDevice({
name = "My Child",
type = "com.fibaro.binarySwitch",
initialProperties = { value = false }
}, ChildClass) -- ChildClass is your class extending QuickAppChild
-- Load existing children at startup (call in onInit)
self:initChildDevices({ ["com.fibaro.binarySwitch"] = ChildClass })
-- Remove a child
self:removeChildDevice(childId)Actions and Interfaces
self:callAction("myMethod", arg1, arg2) -- dispatch to self method
self:addInterfaces({"energy", "battery"}) -- add device interfaces
self:hasInterface("energy") -- returns booleanProperties on self
| Property | Type | Description |
self.id | number | Device ID |
self.name | string | Device name |
self.type | string | Device type string |
self.properties | table | Full properties (read-only reference) |
fibaro.* Global API
Device Control
fibaro.call(deviceId, "turnOn") -- invoke action on device
fibaro.call(deviceId, "setValue", 75) -- with argument
fibaro.getValue(deviceId, "value") -- returns value only
fibaro.get(deviceId, "value") -- returns (value, modifiedTimestamp)
fibaro.getDevicesID(filter) -- returns array of IDs matching filterSince fw ≥ 5.031.33,
fibaro.callis async by default. For self-calls useself:method(). Control withfibaro.useAsyncHandler(true/false).
Device Metadata
fibaro.getName(deviceId)
fibaro.getType(deviceId)
fibaro.getRoomID(deviceId)
fibaro.getRoomName(roomId)
fibaro.getRoomNameByDeviceID(deviceId)
fibaro.getSectionID(deviceId)Global Variables
fibaro.getGlobalVariable("myVar") -- returns string value
fibaro.setGlobalVariable("myVar", "val") -- set valueScenes & Profiles
fibaro.scene("execute", sceneId)
fibaro.scene("kill", sceneId)
fibaro.profile(profileId, "activateProfile")Alarms & Notifications
fibaro.alarm(partitionId, "arm") -- arm/disarm partition
fibaro.alert("email", {userId}, "msg") -- send notification
fibaro.alert("push", {userId}, "msg")
fibaro.emitCustomEvent("myEventName") -- fire custom eventFull notification landscape (direct push vs push-by-user vs simplePush vs e-mail vs
notification centre, phone/user ID semantics, no critical push):
assets/notifications.md.
Logging (global)
fibaro.debug(tag, message)
fibaro.trace(tag, message)
fibaro.warning(tag, message)
fibaro.error(tag, message)urlencode (global)
urlencode(str) -- HC3-style URL percent-encoding: "\n" becomes "\r\n" first,
-- space -> %20, unreserved -_.~ kept, other bytes -> %XX
-- (multibyte text encoded per UTF-8 byte). nil passes through.plugin.* API
Lower-level API — prefer self: QuickApp methods where available.
plugin.mainDeviceId -- current device ID
plugin.createChildDevice(props) -- create child device
plugin.deleteDevice(deviceId)
plugin.getChildDevices(deviceId)
plugin.getDevice(deviceId)
plugin.getProperty(deviceId, prop)
plugin.restart(deviceId)api.* — Direct REST Calls to HC3
local device = api.get("/devices/25")
local devices = api.get("/devices?interface=zwave")
local result = api.post("/customEvents/myEvent", {})
api.put("/devices/25", { name = "New Name" })
api.delete("/globalVariables/oldVar")api.* returns the parsed JSON response (Lua table), or nil on error.
Write safety:
api.put/api.postagainst HC3 have several non-obvious silent-failure modes (partial array writes wipe siblings, action POSTs return HTTP 202 even on failure, enum values get silently coerced, etc.). Before writing non-trivial data viaapi.*, see the hc3-write-safety skill for the read-modify-write + verify pattern.
Timers and Scheduling
-- One-shot timer
local ref = setTimeout(function()
self:debug("fired!")
end, 5000) -- ms
clearTimeout(ref)
-- Repeating timer
local ref2 = setInterval(function()
self:debug("tick")
end, 10000)
clearInterval(ref2)
-- setTimeout with 0 yields to pending tasks immediately
setTimeout(function() self:doSomething() end, 0)Avoid
fibaro.sleep(ms)— it blocks all event handling, HTTP callbacks, and timers while sleeping. Always usesetTimeout/setIntervalpatterns instead.
net.* — Networking in QuickApps
The mental model: offline mode simulates the HC3's REST API — but
net.* is always real network. A QA in the offline sim can still reach
the internet, and that is the point (test your Hue/Tibber/weather
integration without the controller). Mock the far end with net.*Server
(flua extension) when you want determinism instead of real services.
- Routing: a completed HTTP exchange — any status, 4xx/5xx included —
reaches
success; only transport failures (DNS, refused, timeout) reacherror. The error argument is the underlying system message ("timed out","[Errno 61] Connection refused", ...) — match on presence, not equality. TCP reads surface"timeout"/"closed"and prefixed op errors ("connect: ...","send: ..."). - SSE: a
text/event-streamresponse streams —successfires once per raw event block ({status, data, headers}), the stream ends silently when the server closes (HC3 behavior), and a mid-stream read timeout is a transport failure →error. Real services send keepalives just inside typical read timeouts (Hue ≈ every 58s) — treat mid-stream timeouts as transient: catch and reconnect, don't fail the QA. - WebSocketClientTls verifies nothing by default — the HC3 offers no
way to supply a certificate for websockets (undocumented by Fibaro), so
self-signed bridge certs (Dirigera etc.) just work; flua matches that.
connect(url, headers, {checkCertificate = true})is the flua extension that opts back into verification. - exit(): cancels the QA's in-flight requests — an open SSE stream dies with its QA.
- Full semantics and the mock-server API: USAGE.md "Network clients" and
"Mock servers"; living examples:
examples/http.lua,examples/tcp.lua,examples/mockServer.lua,examples/scenes.lua.
HTTP Client
local http = net.HTTPClient()
http:request("https://api.example.com/data", {
options = {
method = "GET",
headers = { ["Authorization"] = "Bearer " .. token }
},
success = function(response)
local data = json.decode(response.data)
self:debug("status:", response.status)
end,
error = function(err)
self:error("HTTP error:", err)
end
})TCP Socket
local tcp = net.TCPSocket()
tcp:connect("192.168.1.100", 8080, {
success = function()
tcp:write("HELLO\n", {
success = function() self:debug("sent") end
})
end,
error = function(err) self:error("connect error:", err) end
})
tcp:read({ success = function(data) self:debug("received:", data) end })
tcp:close()UDP Socket
local udp = net.UDPSocket()
udp:sendTo("data", "192.168.1.100", 9999)JSON
local t = json.decode('{"key": "value", "num": 42}')
local s = json.encode({ key = "value", num = 42 })Availability: flua vs HC3
| Function | flua | HC3 |
json.encode(t) | ✓ | ✓ |
json.decode(s) | ✓ | ✓ |
json.encodeFormated(t) | ✓ | ✗ — flua only |
json.util.InitArray(t) | ✓ | ✓ |
json.encodeFormatedproduces pretty-printed JSON but is only available in flua. Do not use it in code intended to run on a real HC3 — it will throw a nil-call error. If you need formatted output on the HC3, provide your own implementation.
Marking tables as arrays: json.util.InitArray
By default, an empty or mixed Lua table may encode as {} (object) rather than [] (array). Use json.util.InitArray to force array encoding — this is available on both flua and the real HC3 and is essential when posting to REST APIs that require a JSON array:
local arr = json.util.InitArray({}) -- encodes as [] not {}
local items = json.util.InitArray({"a","b"}) -- encodes as ["a","b"]
api.post("/foo", { ids = json.util.InitArray({}) })Fields that MUST be array-encoded when uploading an FQA
When building an FQA structure to POST to /api/quickApp/, several
nested fields are array-typed on HC3. If any of them is empty (or
sparse), the JSON encoder will emit {} and HC3 will reject the upload
or silently treat the field as missing. Mark them as arrays before
encoding:
local function markArray(t) if type(t)=='table' then json.util.InitArray(t) end end
markArray(fqa.initialInterfaces)
markArray(fqa.initialProperties.quickAppVariables)
markArray(fqa.initialProperties.uiView)
markArray(fqa.initialProperties.uiCallbacks)
markArray(fqa.initialProperties.supportedDeviceRoles)
api.post("/quickApp/", json.encode(fqa))The same care applies whenever you PUT an array-valued property back to
/api/devices/{id} (e.g. properties.quickAppVariables,
properties.uiCallbacks) — see the hc3-write-safety skill for the
full list of array-valued properties and the read-modify-write pattern
needed to avoid wiping siblings.
Standard Lua Available in QAs
os.time(), os.date(), os.clock(), os.difftime()
string.format(), string.find(), string.gmatch(), string.gsub(), string.sub(), string.split() (Fibaro extension), string.starts() (Fibaro extension)
table.insert(), table.remove(), table.sort(), table.concat()
math.floor(), math.ceil(), math.abs(), math.max(), math.min(), math.random(), math.huge
pcall(f, ...) — protected call; returns (true, result) or (false, errmsg)
tostring(), tonumber(), type(), pairs(), ipairs()
Key Caveats
self:getVariable(name)returns""when missing, notnilself:updateView(...)values must be strings — usetostring(n)self.properties.*writes are transient — useself:updateProperty()to persist- All QuickApp methods are publicly callable via
fibaro.call(id, "method")— keep private helpers aslocal functions outside the class self.parentis not available duringChild:__init()— use child'sonInit()insteadquickAppglobal is only set afteronInit()returns
See also
- hc3-write-safety — silent-failure hazards on
api.put/api.postwrites (partial array wipes, enum coercion, Z-wave cache-only writes, HTTP-202 action failures) and the defensive read-modify-write pattern. - hc3-rest-api — full HC3 REST endpoint reference.
- quickapp-patterns — common QA implementation patterns (timer loops, refreshStates, child devices).
- quickapp-troubleshooting — known QA quirks and HC3 firmware behaviour differences.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

