agentleFS
jangabrielssonjangabrielsson/flua/.github/skills/quickapp-api/SKILL.md

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.

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
  1. Very specific:

    Every QuickApp starts with onInit().

    line 14
  2. Distinctive:

    All logging methods accept multiple arguments separated by commas.

    line 34
  3. Very specific:

    silently: when the server closes (HC3 behavior), and a mid-stream read timeout is a transport failure → error.

    line 239
  4. Very specific:

    When building an FQA structure to POST to /api/quickApp/, several nested fields are array-typed on HC3.

    line 323
  5. Very specific:

    All QuickApp methods are publicly callable via fibaro.call(id, "method") — keep private helpers as local functions outside the class

    line 369

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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.
end

QuickApp Methods (self:...)

Logging

self:debug(...)    -- DEBUG level — shown in HC3 debug console
self:trace(...)    -- TRACE level
self:warning(...)  -- WARNING level
self:error(...)    -- ERROR level

All 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 generated

Common 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 string

Children

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 boolean

Properties on self

PropertyTypeDescription
self.idnumberDevice ID
self.namestringDevice name
self.typestringDevice type string
self.propertiestableFull 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 filter

Since fw ≥ 5.031.33, fibaro.call is async by default. For self-calls use self:method(). Control with fibaro.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 value

Scenes & 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 event

Full 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.post against 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 via api.*, 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 use setTimeout/setInterval patterns 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) reach error. 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-stream response streams — success fires 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

FunctionfluaHC3
json.encode(t)✓✓
json.decode(s)✓✓
json.encodeFormated(t)✓✗ — flua only
json.util.InitArray(t)✓✓

json.encodeFormated produces 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, not nil
  • self:updateView(...) values must be strings — use tostring(n)
  • self.properties.* writes are transient — use self:updateProperty() to persist
  • All QuickApp methods are publicly callable via fibaro.call(id, "method") — keep private helpers as local functions outside the class
  • self.parent is not available during Child:__init() — use child's onInit() instead
  • quickApp global is only set after onInit() returns

See also

  • hc3-write-safety — silent-failure hazards on api.put / api.post writes (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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.