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.
Hubitat code can't run on a CI runner, so testing means stubbing the hub runtime around the logic — the testing-standards Platform-Bound carve-out. The shape:
def script = new HubitatDeviceSandbox(new File("device.groovy")).run(api: Mock(DeviceExecutor))
script.on()
then: 1 * api.sendEvent([name: "switch", value: "on"])skills/test/example.md has the full template — build.gradle (hubitat_ci 0.17 on the biocomp Azure feed) plus runnable app and driver Spock tests — verified 2026-07-16 by running it green. Use it, including its JDK 11 toolchain pin: hubitat_ci 0.17 is binary-locked to Groovy 2.5, which does not run on JDK 16+, so the suite cannot execute on a default modern JDK. example.md has the evidence and the failure signatures.
Identify the pure logic worth testing — value conversions, parse() decoding, state transitions, option handling — versus the thin calls into the platform. The logic is testable; the platform calls get mocked on the executor. If the driver is all I/O with no branching logic, say so — the honest path is then live debug. Proceed to Step 2.
Add hubitat_ci and Spock per the example.md build.gradle (hubitat_ci ships on the biocomp Azure feed, not Maven Central), and keep its JDK 11 toolchain pin. Create a test that constructs HubitatAppSandbox/HubitatDeviceSandbox from the script file. sandbox.run() compiles it and validates metadata/preferences/capabilities; sandbox.run(api: mockExecutor) gives you a mocked AppExecutor/DeviceExecutor to assert platform calls against. Proceed to Step 3.
Per testing-standards: fixed inputs (a captured Zigbee/Z-Wave description string is a fine fixture), assert outcomes (1 * api.sendEvent([name: "switch", value: "on"])), no wall-clock or random data, each test independent. Cover the capability commands and the parse() dispatch against known frames. Proceed to Step 4.
Run the suite with Gradle. On failure, read the mode:
sandbox.run() (bad input type, unsupported API, bad command signature) is usually a script bug — fix the driver, it would fail on the hub too. Confirm before editing the app: hubitat_ci's validator is stricter than the hub on definition() metadata, so mandatory parameters '[iconX3Url]' not set is the harness talking, not the platform (example.md Notes). Relax it with validationFlags; never add junk to a working app to satisfy the harness.example.md).0 * ... sendEvent) means the code path wasn't reached — check the branch and inputs.hubitat_ci stub gap on a newer API — fall back to extracting the logic and testing it with Spock + groovy.mock.interceptor.Iterate until green, then proceed to Step 5.
Wire the green suite into the repo's CI so it runs on every change. Install a JDK 11 in CI — the toolchain pin selects a JDK, it does not provide one, and a runner without it fails with No matching toolchains found. Provision it explicitly (actions/setup-java with java-version: 11, kept alongside the JDK the Gradle runtime needs), or enable Gradle's toolchain auto-provisioning. Never drop the pin to match whatever JDK the runner ships — that is the JDK ceiling reasserting itself, and the suite will not run.
Document a manual validation procedure for the device-I/O layer that can't run off-hub (what to deploy, what to trigger, what to observe).
State plainly what a green run does not prove. The harness models state as a plain in-memory Map; the real state round-trips through JSON between every execution. The suite is blind to non-JSON-serializable state values, to key-type changes across the round-trip, and to restore-only failures. A green suite is evidence about the code under test, never about the harness or the plumbing around it. Finish here.