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.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
The hubitat-dev toolset is HTTP/code only. A class of operations has no documented endpoint and
is reachable only through the hub web UI at http://<hub-ip>:8080, driven with the Playwright MCP.
The setup, full workflow, selectors, and per-gotcha detail all live in skills/_reference/playwright-ui.md.
skills/_reference/endpoints.md).POST /installedapp/update/json with _action_remove (skills/_reference/endpoints.md, app-removal note). It even fires on a disabled removeButton:false app. Keep the read-the-confirm-first discipline as a human checkpoint (below), but the mechanism no longer needs the UI./apps/api/<appId>/dashboard/<appId>/layout, not N clicks in the tile editor (skills/_reference/endpoints.md)./installedapp/configure/json/<appId>/<page> logs an app ERROR and pushes a notification to a human (skills/_reference/endpoints.md)._action_href buttons, never by probing plausible names.skills/_reference/playwright-ui.md)._action_update on POST /installedapp/update/json. It is not the counterpart of the _action_remove route._action_update sent with Content-Type: application/json returns 200 and runs nothing (skills/_reference/endpoints.md).scheduledJobs is the load-bearing check that a config commit ran. A surviving subscription and a handler-written state value both read healthy across a failed commit._action_remove is branched on before settings are applied._action_update applies settings and clears anything omitted (skills/_reference/playwright-ui.md).name="_action_update" / id=btnDone, never on position and never on visible text.skills/device-migration/SKILL.md).label.is-checked CSS class. Read the class, never input.checked — the property is unreliable and may or may not track, depending on element and platform version.browser_click / browser_type. element.click() inside browser_evaluate does not fire jQuery/Vue/MDL handlers.refs are unreliable on Hubitat's MDL <div> controls — a ref resolves to a wrapper and the click hits a container, silently. Tag the real control by walking up from its hidden settings[...] input, then click the tag: skills/_reference/playwright-ui.md gotchas 10–12. Tagging in browser_evaluate is not the banned synthetic click.label.is-checked, MDL <div>, unreliable-ref guidance here applies to them. The device edit page on 2.5.1.135 is PrimeVue (p-inputtext, p-tabview-panel, data-pc-section / data-pc-name); confirm which framework a page uses before assuming a selector strategy.TabView panel renders display:none until its tab is clicked. The Device label input (for POST /device/update) sits in the Device Info panel; Playwright times out filling an invisible element, which reads like a selector bug. Click the tab first, then address it input[inputid="Device label"] (these inputs carry no name or id) (skills/_reference/endpoints.md).POST /installedapp/update/json, form-urlencoded (skills/_reference/endpoints.md)..checked or dispatching synthetic events does not persist.settings[<name>] write is a shortcut only for an already-filled device input — set its hidden value directly and Done serializes it. A never-set input renders device-btn-empty whatever its required: value, and writing the hidden value does not flip that class. An empty optional input then makes Done a silent no-op; an empty required one rejects. Both need the picker or a fill() flip (skills/_reference/playwright-ui.md gotchas 14, 17).submitOnChange gates — the dependent controls do not exist until the picker's Update commits.device-save picker. Newer inline Vue pickers (Room Lighting activation-options switch guards, switchesD/switchesOE) mount inline under the button, not in #deviceListModal (a dead shell): filter with real keystrokes (locator.fill() doesn't trigger the Vue filter) and click the checkbox and its div.mdl-button Update by coordinate — a label-locator click collapses the dropdown (skills/_reference/playwright-ui.md gotcha 26).browser_select_option sets the native select.value and reports success, but the widget never fires submitOnChange and the page never advances — drive the widget (real-click p.CaptionCont to open, then the li), same as RL's enum guards (skills/_reference/playwright-ui.md gotcha 30).class="… device-save" but its tag and DOM position both vary — <div> vs <button>, and sometimes mounted outside the input's #<name>-options container. Query document-wide and filter to the visible one (offsetParent); match by class + visibility, never by tag and never by container (skills/_reference/playwright-ui.md gotcha 12).configure/json after every change, then re-read the app's live surface when the change is meant to alter behavior.input[name="settings[<name>]"]: "" until the picker's Update commits, a comma-separated id list after. Compare as a set — the order is selection order, not sorted.rules/device-lifecycle.md Audit live consumers separately.statusJson.settings as the configured-input inventory.rules/device-lifecycle.md.mainPage and its sub-pages use different table column layouts — identify a column by its hidden settings[...] input name or by content, never by index across pages.configure/<id>/mainPage is a stub — no settings[...], removeButton:false, and a rendered control set that varies by build (Cancel/Remove/Enable on 2.5.1.134; Enable alone on 2.5.1.x/RM 5.1.8). Enable it first (POST /installedapp/disable {"id":<id>,"disable":false}) or an empty settings set reads as an empty rule. Note the hidden Remove button does not mean un-removable — HTTP removal works regardless (skills/_reference/playwright-ui.md gotcha 31).statusJson.eventSubscriptions, never at its config page.rules/device-lifecycle.md).POST /installedapp/disable off-and-on and RM's own Update Rule button both leave it at zero (skills/_reference/playwright-ui.md).actType.N/… settings behind — a present settings[N] does not mean action N exists. Verify against the rendered action rows or the "Select Actions to Run" summary (skills/_reference/playwright-ui.md gotcha 33).skills/_reference/playwright-ui.md gotcha 32).browser_snapshot and DOM reads, not browser_take_screenshot.modes: ["0"] means All Modes. It is not mode id 0./modes/json.modeXD.modeXOff.scheduledJobs[].prevRunTime: null records no previous firing for the current schedule. It does not mean the job is disabled.? means its template names an attribute the bound device does not have.? tile is not a dead sensor report. Read the bound device's currentStates for the template's attribute before concluding the sensor failed.name="_action_remove", never the visible text — the same page carries a same-labelled removeDeviceAndCallback() device-removal decoy (skills/_reference/endpoints.md, app-removal note).POST /installedapp/update/json with the minimal _action_remove body (skills/_reference/endpoints.md). Prefer it over UI-driving, but keep the read-the-confirm-first human checkpoint: read version fresh from configure/json/<id>/mainPage, confirm the target app by name, then POST. Verify after via statusJson ({}) and absence from /hub2/appsList.swVal:"on" even with the light physically off at Done (skills/_reference/playwright-ui.md gotchas 5, 40). Add members, then set each device's captured state directly (Level cell → dimLA input; Switch cell → on/off toggles; or edit any cell via buttonClick(this), gotcha 40). Read the capture table before Done regardless — on(off) is correct, off(off) is not. Avoid "Re-Capture" unless the physical lights already hold the desired state.button[id="settings[runAction]"] ("Run Actions") executes the rule's actions immediately — a live side effect, not navigation. Target it as an attribute selector (the bracketed id is not a CSS id-selector) and read the button before clicking (skills/_reference/playwright-ui.md gotcha 35).removeButton: false means two different things. A disabled app renders removeButton:false incidentally, yet HTTP removal still works (above). Apps that set removeButton: false by design (e.g. HubiThings Replica) are the untested case — record them as remove-not-automatable until someone confirms otherwise; do not generalize the disabled-app result to them.