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
These pass the sandbox compiler and then misbehave at runtime. They are the bulk of "it saved fine but does nothing" reports.
subscribe(dev, "motion", "motionHandler"), runIn(300, "checkState"), schedule(cron, "poll") reference the handler by a bare string. A typo or a renamed-but-not-updated handler compiles clean and fails silently at dispatch time.runIn/runOnce/schedule string for the old name. The lint-review skill checks that each such string resolves to a defined method.if (offAfter) is false for both null and 0. Code that means "was a value provided?" but writes if (setting) mistakes a legitimate 0 for unset.if (offAfter != null).required device input is null until selected. lights.on() throws when lights is null.lights?.on()) or mark the input required: true.type changes the picker's form, not the value already stored. An instance first saved with type: "capability.thermostat" still hands back a DeviceWrapper from settings.thermostat after the code switches that input to type: "enum" (String id) — the new code throws No signature of method … DeviceWrapper.settings as a plain map with the author's type — both correct. Only a real instance carried across the type change holds the old type, so CI and a re-install stay green while the user who upgraded in place is the one who hits it. It took down the config page and would take down initialize() and every handler while the instance stayed subscribed.String id = (settings.thermostat instanceof String) ? settings.thermostat : null — never settings.thermostat.findAll { … }, which iterates a String's characters and yields a list of one-char strings, mangling the id. Filter with findAll { it instanceof String } only when the value is a multi-select List. A device value assigned to String id = settings.foo coerces via toString() (wrong id, no throw); passed to someMethod(String) it throws MissingMethod.getObjectClassName(v) — not getClass() (sandbox-banned, rules/sandbox-constraints.md) — and log it so the cause is visible. Sibling of the state-side DeviceWrapper trap (rules/state-vs-attributes.md), reappearing from settings where newer code expects a scalar. Observed on 2.5.1.128.removeSetting is deferred — not a same-execution guardapp.removeSetting(name) does not update the in-memory settings map within the same execution. A dropIncompatibleSettings() that removes the setting then reads it again later in the same render still reads the OLD value and still throws.updateSetting/removeSetting/clearSetting are otherwise undocumented (rules/app-lifecycle.md).hubitatQueryString (reserved — holds the JSON of URL query params).state, device, location, settings, log, app).name + namespace in definition must be globally unique on the hub.e.statusCode throws from inside its own getterhttpGet/httpPost can raise a groovyx.net.http.HttpResponseException whose internal response is null. getStatusCode() dereferences it, so reading e.statusCode throws NPE.e is not null, and e?.statusCode still enters the getter. The NPE escapes the catch, so every recovery below it is dead code and the real error is never logged.e.response?.data is null for the same reason. A (statusCode == 500 && e.response?.data?.status?.code == 14) test cannot match on this platform even after the NPE is stopped — fixing only the crash leaves the bug.e?.response?.status, and returns null when both are unreadable. Branch on a null status with a check that does not need it (compare atomicState.authTokenExpires to now()).state[...] are safestate["pending${zone}"] = true is not a bug. The subscript operator normalizes the key to String on write, so the entry survives state's JSON round-trip and a later GString lookup hits.map.put("pending${zone}", v) and to GString keys in map literals — not to the subscript assignment path. After an explicit put, map.get("pendingKitchen") returns null.@Field static variable is shared across all instances of that app/driver and is lost on reboot or code re-save. It is not per-device storage — use state for that (see rules/state-vs-attributes.md), or a ConcurrentHashMap keyed by device id if you genuinely need shared, rebuild-on-boot data.