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
When a Hubitat hub lives on an isolated IoT VLAN and the Apple Home controllers live on a
trusted Main VLAN, "enable mDNS" alone does not make HomeKit work. HomeKit needs two
distinct paths, and a prebaked "allow mDNS" firewall exception satisfies only the first
half of the first one. Verified working on a live UniFi deployment: Apps Hubitat at
192.168.30.15 (IoT 192.168.30.0/24) publishing HomeKit bridges to an Apple Home controller
at 192.168.10.158 (Main 192.168.10.0/24), across a UniFi gateway. All Hubitat hubs here are
production — this is a live-network procedure, not a lab (multi-hub-topology rule; the same
production-safety stance as device-lifecycle).
HomeKit bridges advertise _hap._tcp over mDNS, which is link-local — it does not cross a
VLAN boundary on its own. A reflector/proxy has to carry it:
_hap._tcp.IoT → Gateway deny — a blanket IoT-to-gateway
block eats the advertisements before the proxy ever sees them.The firewall exception that actually worked, placed before Block IoT to Gateway:
| Field | Value |
|---|---|
| Source zone | Internal |
| Source | 192.168.30.0/24 (IoT) |
| Source UDP port | 5353 |
| Destination zone | Gateway |
| Destination UDP ports | 5353,32768-60999 |
| Protocol | UDP |
| State | All |
Destination 5353 is mandatory, not optional. The high range 32768-60999 covers the
stateful replies to legacy-unicast queries the reflector sends — but unsolicited HomeKit/mDNS
advertisements arrive on 5353, so a rule allowing only the high range passes the query replies
and silently drops the advertisements. That is the trap the out-of-the-box "allow mDNS" preset
falls into.
Coexistence, verified: Block IoT to Gateway Management and Block IoT to Default Management
both stay enabled with the mDNS exception above in front of them.
The advertisement carries the bridge's address and its HAP TCP port in the _hap._tcp SRV
record. Discovery finding the bridge is not the same as a controller being able to talk to it:
Block IoT to Main (NAS isolation) is the rule that severs this path. It is currently paused.
Do not re-enable it without first proving all three: Main can open TCP to each bridge's advertised
_hap._tcp port, established/related return traffic from IoT is allowed, and HomeKit reconnects
after an integration restart.
For a Hubitat on a UniFi access port:
After any firewall change, use Hubitat's non-destructive "Restart integration" action only, then verify from a Main-network host:
dns-sd -B _hap._tcp local. # the Hubitat bridges appear in the browse list
dns-sd -G v4 hubitat.local. # resolves to 192.168.30.15Then confirm Hubitat's HomeKit page shows an active connection from a Main-network controller.
Known-good result on this deployment: Apps HomeKit connected from 192.168.10.158. A full service
check restarts only the bridge's HomeKit service and confirms it disappears, republishes, and
reconnects.
Work the path, not the symptom — top to bottom, stop when it breaks:
dns-sd -B).Export is one HTTP GET — GET /hub/homekit/enableDevice/<deviceId>/<true|false>, no app Done required
(skills/_reference/endpoints.md). Ground truth for what is actually served is the unauthenticated HAP database
at GET http://<hub-ip>:21063/accessories, not the app's authorizedDevices setting, which records what was asked
for.
HomeKit allows 149 accessories + the bridge = 150 per bridge, enforced client-side by the app
(if (checked || deviceCount < 150)). On a multi-hub setup that is not a preference — it decides the layout, and it
produces the rule in rules/multi-hub-topology.md: the hub that physically owns a device exports it, and a
hub-mesh mirror is never exported. Measured on the fleet 2026-08-30: the apps hub's bridge exports 104 devices, of
which zero are mesh mirrors, while every Zigbee sensor is exported from the hub that owns its radio.
Accessory class is capability-derived and not freely settable, which constrains what an export can achieve:
| device | classes offered |
|---|---|
| Dimmer / colour bulb / CoCoHue group | Light, Dimmable Light, Colour Temp Light, RGB Light, Dimmer, Switch |
| Zooz ZEN switch (wall lights and exhaust fans) | Switch, Button — nothing else |
| Zooz ZEN plug | Outlet, Switch, Smoke Detector, CO Detector — no Light |
| Zooz ZAC36 ball valve | Temperature Sensor, Valve, Water Sensor |
So a switch-only device cannot be exported as a Lightbulb or a Fan. Class changes go through the app form
(buttonClick + confirm), not a hub endpoint — enableDevice is the only /hub/homekit/ route on that page, and
the controls stay hidden until Advanced → Show accessory classes and characteristics is toggled on.
HAP strips a trailing ) from names, so Kitchen (Hue Group) arrives as Kitchen (Hue Group.
Grounded 2026-08-11 on 2.5.1.140 with both bridges paired and serving, re-exercised 2026-08-30 on 2.5.1.169 adding
a Zigbee leak sensor: one GET moved authorizedDevices 53 → 54 and the accessory appeared in the HAP DB ~10 s later.
A blocked mDNS or HAP path looks exactly like a broken integration — and none of the destructive "fixes" repair a network problem. Until the paths above are proven, do not: factory-reset a hub, reset HomeKit pairing, regenerate the QR/pairing code, reboot or power-cycle a hub, or move a hub between VLANs. "Devices" Hubitat is production, not a disposable test hub. Every one of these needs explicit authorization, and none of them can undo a firewall rule.