CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/hubitat-dev

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

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

example.mdskills/test/

hubitat_ci — worked example

Verified 2026-07-16 by running it — Gradle 8.4, this build.gradle, the app test below, tests=2 skipped=0 failures=0. HubitatAppSandbox/HubitatDeviceSandbox load a script with GroovyShell, validate its metadata/preferences/capabilities, and let you drive its methods with a mocked executor.

build.gradle

hubitat_ci is published on the biocomp Azure feed, not Maven Central or jitpack:

plugins { id 'groovy' }

sourceSets { test { groovy { srcDirs = ['src'] } } }   // non-standard layout

// Not optional — see "The JDK 11 ceiling". Gradle itself runs on a modern JDK;
// compile and test fork to the toolchain, so Gradle 9 (needs JDK 17+) still works.
java { toolchain { languageVersion = JavaLanguageVersion.of(11) } }

repositories {
    mavenCentral()
    maven { url 'https://biocomp.pkgs.visualstudio.com/HubitatCiRelease/_packaging/hubitat_ci_feed@Release/maven/v1' }
}

dependencies {
    testImplementation 'org.codehaus.groovy:groovy-all:2.5.4'
    testImplementation 'org.spockframework:spock-core:1.2-groovy-2.5'
    testImplementation 'me.biocomp.hubitat_ci:hubitat_ci:0.17'
}

testCompile was removed in Gradle 7.0. A build using it cannot evaluate at all, failing with Could not find method testCompile() — a configuration failure, not a test failure.

The JDK 11 ceiling

Run this suite on JDK 11. Verified 2026-07-16 across the JDKs a current machine actually has:

ToolchainResult
JDK 11tests=2 failures=0 — the suite runs
JDK 16 / 17 / 21Could not initialize class org.codehaus.groovy.vmplugin.v7.Java7
JDK 25Unsupported class file major version 69

hubitat_ci 0.17 is binary-locked to Groovy 2.5, and Groovy 2.5 does not run on JDK 16+. Do not raise either pin to match a newer runtime — Groovy 4 fails on NoClassDefFoundError: groovy/util/slurpersupport/GPathResult, Groovy 3 on NoSuchMethodError in DefaultGroovyMethods, and neither is fixable downstream. --add-exports does not rescue JDK 16+ either; the CHANGELOG records the full investigation.

The ceiling is hubitat_ci's, not Hubitat's. The hub runs Groovy 2.4; the harness stays newer than production.

Renewal: the three pins move together (Groovy 2.5 ↔ Spock 1.2 ↔ hubitat_ci 0.17), and the coupling lives in Spock's artifact namespock-core:1.2-groovy-2.5 — where no scanner can read it. The Azure feed has no dependency scanner; check the feed's own metadata, which is authoritative and machine-readable:

curl -s 'https://biocomp.pkgs.visualstudio.com/HubitatCiRelease/_packaging/hubitat_ci_feed@Release/maven/v1/me/biocomp/hubitat_ci/hubitat_ci/maven-metadata.xml'

As of 2026-07-16 it reports <release>0.17</release>; 0.18 returns HTTP 404. Do not renew against the GitHub releases page alone — it tops out at v0.16, older than the pinned 0.17, so it under-reports what the feed actually serves.

Cadence: check that metadata when adopting the template and on each dependency-review pass. If a newer hubitat_ci ever ships, bump all three pins and re-check the JDK ceiling as one focused change.

App test — validation + a mocked callback

import me.biocomp.hubitat_ci.api.app_api.AppExecutor
import me.biocomp.hubitat_ci.api.common_api.Log
import me.biocomp.hubitat_ci.app.HubitatAppSandbox
import spock.lang.Specification

class MyAppTest extends Specification {
    HubitatAppSandbox sandbox = new HubitatAppSandbox(new File("appscript.groovy"))

    def "metadata and preferences validate"() {
        expect: sandbox.run()          // compiles, builds the object, validates definition()/preferences()
    }

    def "installed() logs the configured number"() {
        setup:
            def log = Mock(Log)
            AppExecutor api = Mock { _ * getLog() >> log }
            def script = sandbox.run(api: api, userSettingValues: [Num: 42])
        when:  script.installed()
        then:  1 * log.info("Installed with number = 42")
    }
}

Driver test — assert on sendEvent and parse()

import me.biocomp.hubitat_ci.api.device_api.DeviceExecutor
import me.biocomp.hubitat_ci.device.HubitatDeviceSandbox
import spock.lang.Specification

class MyDriverTest extends Specification {
    HubitatDeviceSandbox sandbox = new HubitatDeviceSandbox(new File("device.groovy"))

    def "on() emits switch=on"() {
        setup:
            DeviceExecutor api = Mock()
            def script = sandbox.run(api: api)
        when:  script.on()
        then:  1 * api.sendEvent([name: "switch", value: "on"])
    }

    def "parse() decodes a fixed frame"() {
        setup:
            def script = new HubitatDeviceSandbox(new File("device.groovy")).run()
        expect:
            // fixed input string as fixture — never generated, never time-relative
            script.parse("catchall: 0104 0006 01 ...") == [name: "switch", value: "off"]
    }
}

Notes

  • sandbox.run() validates by default; pass validationFlags: [...] to relax specific checks.
  • The validator is stricter than the hub. hubitat_ci 0.17 treats description, iconUrl, iconX2Url and iconX3Url as mandatory in definition(), failing with mandatory parameters '[iconX3Url]' not set. The hub accepts apps without them. Reach for validationFlags; never edit a working app to satisfy the harness.
  • The executor mock (AppExecutor / DeviceExecutor) is where platform calls land — assert on sendEvent, getLog(), HTTP, scheduling as Spock interactions.
  • Where hubitat_ci can't stub a newer API, extract the logic into a plain method and test it with Spock + groovy.mock.interceptor (MockFor/StubFor) instead.

README.md

tile.json