hc3-write-safety
Skill1 starsChanged 15 days agoMITView on GitHub
Non-obvious HC3 write-path hazards learned from direct experimentation. Covers silent data loss on PUT /api/devices/{id} and PUT /api/panels/climate/{id} (array-valued properties like quickAppVariables / parameters / schedules fully replace, so partial writes wipe siblings); silent value coercion on the setVariable action endpoint (numeric-looking strings become numbers and break the HC3 UI edit affordance); silent enum coercion on climate zone mode (invalid values become "Manual"); cache-only writes on Z-wave configuration parameters (PUT updates HC3's cached copy without transmitting to the device); silent action failures via HC3's HTTP-202-with-jsonrpc-error-envelope shape; and the read-modify-write + post-write-verify pattern that defends against them. USE FOR: writing to HC3 state via REST from QA code or external callers, or when deciding whether to use a direct api.put() / api.post() call vs a guarded helper.
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
- lua-basicsSkill
- network-device-testSkill
- plua-migrationSkill
- quickapp-apiSkill
- 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
6 rules. About 0 are common, 1 is distinctive, and 5 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.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
If you're writing from a QuickApp via api.put / api.post, or from an external caller, the patterns and gotchas below apply directly.
- Very specific: line 66
Safe pattern: read the full array, modify in place, write back the full array.
- Very specific: line 89
PUT on every state change would reboot the QA every time.
- Distinctive: line 128
Implication: always refetch and verify after a write if the value is in a constrained domain.
- Very specific: line 169
Never use control_device with action: "setVariable" — the HC3_mcp fork hard-rejects this call at the tool boundary for exactly this reason.
- Very specific: line 283
All the HC3_mcp write tools (modify_device, update_climate_zone, update_location_settings, modify_scene, set_quickapp_variable) do this for …
≈ “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
HC3 Write-Path Safety
HC3's REST API has several write behaviours that are not in the Swagger
documentation and will silently corrupt data, fail to take effect, or
report success on a no-op if the caller doesn't know about them. Every
item below was confirmed by direct experimentation, not by reading
docs. Most were discovered while hardening the HC3_mcp server (see
northernRough/HC3_mcp).
If you're writing from a QuickApp via api.put / api.post, or from
an external caller, the patterns and gotchas below apply directly.
Where the HC3_mcp tools (modify_device, update_climate_zone,
update_location_settings, set_quickapp_variable, modify_scene)
are available, they wrap these hazards with read-modify-write and
post-write verification — direct api.put() / api.post() calls skip
the guards.
Note: If you don't have the HC3_mcp server installed, ignore the MCP tool references below and apply the patterns directly via
api.get/api.put/api.postfrom your QuickApp. The hazards and defensive patterns are the important part.
flua note (the emulator): flua's sim models the same write-path rules with two deliberate differences:
PUT /api/devices/{id}merges thepropertiesdict (the real-HC3 array-replace is NOT simulated — the sim is the safe path), and the restart-on-PUT semantics of section 1 apply in the sim (name/visible/enabled/roomID/propertiesall restart the QA;/plugins/updatePropertydoes not). Action POSTs return plain 202s offline — the JSON-RPC error envelope of section 8 is real-HC3-only, so the defensive check is a harmless no-op in the sim.
1. PUT /api/devices/{id} — top-level merges, nested properties.* arrays fully replace
Top-level fields (name, roomID, enabled, visible, etc.) are
merged: submitting one doesn't affect the others. So this is safe:
api.put("/devices/42", { name = "Living Room Light", roomID = 3 })But array-valued fields under properties.* are replaced wholesale:
properties.quickAppVariablesproperties.categoriesproperties.uiCallbacks
Submitting a partial array destroys every entry not in the submission. This PUT looks like "update one variable" but wipes every other quickAppVariable on the device:
-- DANGEROUS — wipes all other quickAppVariables
api.put("/devices/42", {
properties = { quickAppVariables = { { name = "myVar", value = "new" } } }
})Safe pattern: read the full array, modify in place, write back the
full array. Or use set_quickapp_variable / modify_device from the
HC3_mcp server, which do this for you.
local dev = api.get("/devices/42")
local vars = dev.properties.quickAppVariables
for _, v in ipairs(vars) do
if v.name == "myVar" then v.value = "new" break end
end
api.put("/devices/42", { properties = { quickAppVariables = vars } })properties.parameters, properties.associations and
properties.multichannelAssociations are array-valued and have their
own additional silent-write trap — see section 7.
A side-effect every device PUT carries: the plugin restarts. On the
HC3, PUT /api/devices/{id} — top-level fields and properties
writes — restarts the QA (timers cancelled, onInit runs again, runtime
state lost). The one write channel that does NOT restart is
POST /api/plugins/updateProperty (section 6). Treat a device PUT as a
configuration-time write, not a per-event write: writing a variable via
PUT on every state change would reboot the QA every time. Confirmed on
the real HC3: variable writes via PUT restart too — that is exactly why
QuickApp:setVariable goes through /plugins/updateProperty instead
(section 5). flua's sim implements the same model.
2. PUT /api/panels/climate/{id} — same shape as devices
Climate zones have the same top-level-merge / nested-property-replace
semantics. Because climate zones hold weekly schedules (monday,
tuesday, ... as nested objects each containing morning/day/
evening/night), a naive partial write can wipe the entire weekly
schedule.
Safe pattern: read the full zone, deep-merge at the object level
(preserving sibling schedule entries), then PUT the merged object.
update_climate_zone in the HC3_mcp server does this.
Top-level fields (name, active, mode) merge cleanly.
3. PUT /api/panels/location/{id} — flat, top-level merge
Location objects are flat (no nested properties wrapper). Top-level
fields merge, so partial writes are safe here. Read-only fields on the
location record: id, created, modified — HC3 rejects or ignores
attempts to change them. The update_location_settings MCP tool rejects
these at the tool boundary.
4. HC3 silently coerces invalid enum values
Submitting an invalid string for a typed enum does not error. HC3 silently substitutes a default.
Confirmed example: climate zone topLevel.mode:
-- submits "NonsenseMode", HC3 silently stores "Manual"
api.put("/panels/climate/7", { mode = "NonsenseMode" })Implication: always refetch and verify after a write if the value is in a constrained domain. Don't trust HC3's 200 OK.
A side-effect to know: flipping a climate zone from Schedule to
Manual (including via silent coercion) sets properties.handTimestamp
to a non-zero value. To return the zone to Schedule, write
properties.handTimestamp = 0 — do not try to write mode = "Schedule"
at top level, which HC3 rejects.
5. POST /api/devices/{id}/action/setVariable — silent number coercion
Calling setVariable via the action endpoint (e.g. via fibaro.call
or control_device in the MCP) coerces numeric-looking string values
("3.0", "12") to numbers while leaving the variable's declared
type: "string" metadata untouched. The stored value becomes
3 (number) while the UI still thinks this is a string-typed row.
Consequence: the HC3 web UI edit affordance disappears for that row and stays gone until the variable is deleted and recreated via the UI. The variable is still readable from QA code, but has become unmanageable from the UI.
Defensive patterns:
- From inside QA code: always wrap with
tostring()if the variable is declaredstringand the value might look numeric:
This is whyself:setVariable("allowableSpread", tostring(val))quickapp-patternsexamples usetostring()— it's defending against this coercion, not just type hygiene. QuickApp:setVariableitself writes throughPOST /api/plugins/updateProperty— deliberately, to avoid the device-PUT restart (section 1). A rawapi.putofproperties.quickAppVariablesrestarts the QA.- From outside HC3 (MCP, external REST): use
set_quickapp_variablefrom the HC3_mcp server, which reads the declared type and coerces correctly, or write directly viaPUT /api/devices/{id}with the fullproperties.quickAppVariablesarray (see section 1) — that PUT restarts the QA (section 1), so batch variable writes and use the non-restarting property channel (section 6) for frequent updates. - Never use
control_devicewithaction: "setVariable"— the HC3_mcp fork hard-rejects this call at the tool boundary for exactly this reason.
6. POST /api/plugins/updateProperty — the non-restarting property channel
This is the documented way to update a device property WITHOUT
restarting the QA (official Plugins API swagger — operationId
updateProperty; the repo carries the source of truth at
fibaro_api_docs/plugins.json). Every other device write path — PUT
/api/devices/{id} with top-level fields or properties — restarts
the plugin on the HC3 (section 1).
- Inside a QuickApp,
self:updateProperty(name, value)calls this endpoint and also updates the QA's ownself.propertiestable, so Lua code sees the new value immediately. Prefer it for the QA's own properties; don't rewrite workingself:updatePropertycalls intoPUT /api/devices/{id}(that would add a needless restart). - From external callers (other QAs, scenes, MCP, REST clients)
writing another device's properties, this is the channel to prefer —
no restart, no array-replace hazard.
PUT /api/devices/{id}is the restarting alternative.
7. Z-wave mesh-config writes are cache-only — do not use REST
PUT /api/devices/{id} with { properties = { parameters = [...] } }
updates HC3's stored copy of a Z-wave device's configuration
parameters, but does not transmit the new value to the physical
device over the mesh. The post-write verify will report success
(HC3's cached array matches what you submitted) and the HC3 web UI
will display the new value. The physical device behaves on the old.
Confirmed live on firmware 5.202.54 against a Zooz ZEN52 double
relay. PUT-set parameter 2 (LED indicator mode) from default 1 to
0. HC3 stored the new value, web UI showed 0, but the unit's
indicator LED continued lighting whenever a load was on (the 1
behaviour). The cached entry stored only {id, size, value} — no
lastReportedValue or lastSetValue field — consistent with "never
transmitted, nothing to track".
Compounding the trap: the dedicated Z-wave action endpoints that should transmit are not implemented on this firmware:
-- All return HTTP 202 with body
-- {"jsonrpc":"2.0","error":{"code":-3,"message":"not implemented"},...}
api.post("/devices/4753/action/getParameter", { args = { 2 } })
api.post("/devices/4753/action/setParameter", { args = { 2, 0, 1 } })
api.post("/devices/4753/action/reconfigure", { args = {} })Verdict on firmware 5.x: there is no working REST path to set Z-wave configuration parameters. Set them via the HC3 web UI (which uses a native protocol), or by re-including the device with the desired parameters preset.
The same precautionary reasoning extends to
properties.associations and properties.multichannelAssociations
— they are structurally the same "write-to-HC3, expected-to-push-
downstream" pattern as parameters, and HC3's broader Z-wave action
surface is broken on this firmware. Treat as cache-only until proven
otherwise. The HC3_mcp modify_device tool hard-rejects all three
fields at the tool boundary.
8. Action POSTs can fail silently — check the response body
HC3 action endpoints (POST /api/devices/{id}/action/{name},
/api/scenes/{id}/action/{verb}, /api/alarms/v1/partitions/{id}/actions/...,
/api/panels/sprinklers/{id}/actions/..., etc.) return HTTP 202
Accepted even when the action fails. The failure shape is a
JSON-RPC envelope in the response body:
{
"jsonrpc": "2.0",
"id": 0,
"error": { "code": -3, "message": "not implemented" },
"endTimestampMillis": ...,
"startTimestampMillis": ...
}api.post and fibaro.call will both happily ignore that envelope
and return as if the call succeeded. A successful action returns the
same envelope shape with error: null.
Defensive pattern (reusing the section 7 example, which is exactly the silent-failure shape this check catches):
local res = api.post("/devices/4753/action/setParameter", { args = { 2, 0, 1 } })
if type(res) == "table" and res.error ~= nil then
error("HC3 action failed: " .. tostring(res.error.code) .. " " ..
tostring(res.error.message))
endThe HC3_mcp server's central HTTP layer applies this check on every
response since bcc643f, so action failures surface as real errors
rather than silent successes.
9. The safe pattern, summarised
For any non-trivial write to HC3:
- Fetch current state —
api.get("/devices/42")or equivalent. - Modify in memory — merge at the object level, preserving sibling keys; for arrays, modify the full array in place.
- PUT the full merged object.
- Refetch and verify — compare submitted keys against stored values. Throw if they don't match.
- For action POSTs, also inspect the response body for a
non-null JSON-RPC
error(section 8).
All the HC3_mcp write tools (modify_device, update_climate_zone,
update_location_settings, modify_scene, set_quickapp_variable) do
this for you. Prefer them over raw api.put / api.post wherever the
MCP is available.
See also
- quickapp-api —
api.*/fibaro.*/self:*reference, includingjson.util.InitArrayand the list of FQA fields that must be marked as arrays before encoding (closely related to section 1's array-replace hazard). - hc3-rest-api — full HC3 REST endpoint reference.
- quickapp-troubleshooting — additional QA quirks and HC3 firmware behaviour differences not specific to writes.
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.

