Context for developing and debugging Hubitat Elevation apps, drivers, and hub environment — sandbox constraints, lifecycle idioms, capability contracts, plus grounded deploy/log-tail/lint mechanisms.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
Process steps in order. Do not skip ahead.
A replacement device gets a new device id, and every app pointing at the old one silently breaks
(rules/device-lifecycle.md). Settings → Swap Device re-points apps in one shot when it is
available. It often is not, and the fallbacks are not interchangeable — Step 3 picks one by why
the swap is blocked.
This skill moves references. It does not delete the old device — Skill(skill: "device-removal")
owns that. Run this before that skill's delete: a deleted device cannot be swapped from.
Re-homing a device to a different hub over Hub Mesh is the park fallback with the orphaned mesh link as the slot — Step 4 covers it.
The Step 5 verification and the manual path both need this, and it must be read before anything changes:
python3 .tessl/plugins/jbaruch/hubitat-dev/skills/_scripts/hub_device_usage.py --device <id> --ip <addr> --live
python3 .tessl/plugins/jbaruch/hubitat-dev/skills/_scripts/hub_device_usage.py --name "<display name>" --ip <addr> --liveArgument and output contract: skills/_scripts/hub_device_usage.py module docstring. Use --hub <name>
instead of --ip to resolve via hubs.json (hub-config skill). Keep the report — it is the
capture, listing app references by enabled/disabled switch state, dashboards, parentApp, and child
devices. live_audit is separate: enabled does not mean live, and unknown does not mean inert.
Proceed to Step 2.
Navigate to http://<hub-ip>:8080/installedapp/direct/swapDevice (the Settings → Swap Device tile,
subtitled "Replace device across all apps at once"). It redirects to a transient app instance at
/installedapp/configure/<id>/mainPage. Drive it with Playwright per skills/_reference/playwright-ui.md.
The page has two pickers — "Select device to be replaced in apps (old)" and "Select replacement device for apps (new)". The new picker stays disabled until an old device is selected; the hub then filters it to devices sharing at least one capability with the old one.
Read both lists before acting, and never assume a selection stuck — the pickers are Vue
controls that ignore a synthetic selectOption on the underlying <select> (the page's own value
changes while the new list stays empty). Click the real "Click to set" control.
Two hazards before you swap:
setLevel on something that has no such command.If both devices are listed, have the user perform the swap, then proceed to Step 5. If the target is absent, proceed to Step 3.
Do not reach for a fallback until you know which one can work. The reason decides:
skills/_reference/parent-child-devices.md) — the page says so:
"Most child devices are not swappable and are not listed here." A virtual hop cannot lift that
exclusion. The last swap of any chain still targets the child, still ineligible. Go to Step 6.
(Exceptions Hubitat allows: AirPlay, Bluetooth, HomeKit Controller, Tuya, Wiz.)rules/zwave-zigbee-mesh.md) ⇒ nothing to swap to yet. Go to Step 4 and
park.Confirm child-vs-capability against the hub rather than guessing: a device absent from
/hub2/devicesList's top level is a child (children are nested only), and a parentApp in
/device/fullJson/<id> marks an app-owned child. Proceed to the step the diagnosis names.
A virtual device is swappable (verified: [Virtual] … devices appear in the list), which makes it
useful for exactly two jobs:
Create it under Devices → Add Device → Virtual, matching the capability you need (Virtual Switch, Virtual Dimmer, Virtual Lock, …). Each hop is a Step 2 swap and carries Step 2's hazards. After the final hop, delete the virtual device — a parked virtual left behind is a device that answers commands and silently does nothing.
Re-home across hubs over Hub Mesh needs no virtual device. The "old" device is a Hub Mesh
linked device — data.source: Linked in /hub2/devicesList (skills/_reference/parent-child-devices.md).
Removing it on its source hub orphans the link here: it drops to [offline] but keeps its id
and its app bindings — the parking slot, ready-made. Capture its appsUsing[] first
(/hub2/hubMeshJson → sharedDevices[], or Step 1's script). Then remove on the source hub, pair the
replacement natively here, and swap the offline link → the new device as in Step 2. If the orphan is
the only offline device, the swap's "old" pick is unambiguous. With others offline, pick it by the
id/name captured in Step 1 — data.source: Linked marks it as the mesh link. The emptied link is
then a normal removal (Skill(skill: "device-removal")). Proceed to Step 5.
Re-run Step 1's --live command against both devices. Compare two axes:
appsUsing is the reference blast radius. The new device should carry the configured app
references. The old should be empty or list only references you explicitly intend to leave.live_audit is behavioral evidence. For a Rule Machine trigger migration, trigDevs must name
the new device and not the old one.unknown.A swap that reports success in the UI but fails either applicable re-read is incomplete.
Dashboards are not covered by any claim here. Hubitat's doc scopes the swap to "all apps" and says nothing about dashboard tiles. Check the old device's dashboards from the Step 1 capture and re-point any that survived by hand — do not report them as migrated without looking. Proceed to Step 6 if anything did not move; otherwise proceed to Step 7.
The fallback when no swap path exists — the doc's own remedy for an incompatible device, and the
only path for a child device. Work the Step 1 capture: for each app using the old device, open
it, select the new device where the old was selected, de-select the old, configure the new to match
if the app needs it (every app differs), and hit Done — the change does not commit until then,
and not over observable HTTP (skills/_reference/playwright-ui.md).
Re-point traps, all grounded in skills/_reference/playwright-ui.md:
button[name^="_action_href"] to reach it (gotcha 19) rather than calling a setting unreachable.submitOnChange button on a settings[...] id is a live action, not
navigation — "Activate" switches the real lights (gotcha 20).motionsInactive on some instances, not motionsOff
— a swap of motions and motionsOff alone misses it silently.tDev-1 beside tDev1. Verify its re-point via state.trigDevs in the rule's statusJson, never the
raw tDev* setting.Verify each app's configured input via appSettings[] from
GET /installedapp/statusJson/<appId> or the page-specific
/installedapp/configure/json/<appId>/<page>.settings. The former names every resolved device
setting in one call. Its sibling top-level settings field is null
(skills/_reference/endpoints.md). statusJson.childDevices is a separate reliable field: a managed
child such as mZone* is absent from /hub2/devicesList top level, and its id comes from the parent
app (skills/_reference/parent-child-devices.md).
Trust the configured and live re-reads over picker values:
motionsInactive.eventSubscriptions[].typeId proves a subscription-driven app is listening.state.trigDevs proves its trigger even while a Required Expression suppresses
subscriptions.tDev-1 / trigDevsW reference can keep the old device in appsUsing after
trigDevs moved. Report it as blast-radius bookkeeping, not a live trigger.Re-run Step 1's --live command when done. Proceed to Step 7.
State which apps moved, by what path (swap / bridge / park / manual), and name anything left for the user — dashboards, scenes, an app that would not commit, a virtual device still to delete. Never report a migration complete on a UI success message alone; report it on the Step 5 re-read.
Removing the old app (a superseded zone controller, say) is a two-step PrimeVue confirm — a
"Remove … now?" prompt, then a "This will remove N child device(s)…" prompt, both button.p-confirm
labelled "Yes" — and it deletes the app's owned child devices. A dangling reference from another app does not block
it: a stale Rule Machine tDev-1 pointing at a deleted device is inert (skills/_reference/playwright-ui.md
gotcha 22). Agent-initiated Remove/Delete clicks are refused by the auto-mode classifier regardless of
in-chat approval — the user performs the destructive delete (rules/device-lifecycle.md), after
which a fresh retry proceeds.
If the old device is now to be removed, hand off: Skill(skill: "device-removal"). Finish here.