| name | hc3-write-safety |
| description | 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. |
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.post from your QuickApp. The hazards
and defensive patterns are the important part.
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.quickAppVariables
properties.categories
properties.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:
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.
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:
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:
6. POST /api/plugins/updateProperty — avoid from external callers
This endpoint does not appear in HC3's Swagger documentation and its
behaviour is not guaranteed stable across firmware versions when called
directly. The hc3-rest-api skill lists it for completeness.
Inside a QuickApp, prefer self:updateProperty(name, value) — it
goes through the same internal path safely and is the canonical way for
a QA to update its own properties. Don't rewrite working
self:updateProperty calls into PUT /api/devices/{id}.
From external callers (other QAs, scenes, MCP, REST clients) writing
to a different device's properties, prefer PUT /api/devices/{id} —
it's the documented endpoint and goes through the merge/replace
semantics in section 1. modify_device in HC3_mcp wraps the documented
path.
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:
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))
end
The 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, including
json.util.InitArray and 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.