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.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)..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).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.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.