Surface unreachable bulbs as "Not Responding" via HAP#173
Merged
kpsuperplane merged 2 commits intoApr 27, 2026
Conversation
Adds an opt-in `reportOffline` flag (default: false) that marks bulbs as "Not Responding" in HomeKit once they miss `offlineThreshold` consecutive getPilot replies, rather than silently replaying the last known cached state. Reachability is cleared on any successful getPilot reply or any registration / getSystemConfig response from the same MAC, so bulbs that come back online recover immediately. Default behaviour is preserved when the flag is left off. When refreshInterval > 0, the refresh loop now also re-broadcasts discovery on each tick so bulbs returning to the network (or acquiring a new DHCP lease) are re-learned automatically. Refresh-loop failures log at debug instead of error to match the convention established by kpsuperplane#141 and avoid spamming the log every tick for a single offline bulb. Also bundles two narrow defensive fixes in the same code region: - Filter `undefined` fields before merging into `cachedPilot`, so firmware replies that omit `dimming` (e.g. sceneId 14 "nightlight") don't produce NaN Brightness in HomeKit — addresses kpsuperplane#96 / kpsuperplane#101 / kpsuperplane#143 / kpsuperplane#159. - `pilotToColor()` falls back to a neutral-white reading when passed an empty cache entry, so temperature / color set handlers invoked after a timeout wiped the cache don't crash with "Cannot read properties of undefined (reading 'temp')" — addresses kpsuperplane#145. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
With refreshInterval > 0 the refresh loop now re-broadcasts discovery on each tick, which used to log one info-level line per listed device per tick (7 lines / 30s in a typical setup). Two follow-ups to keep this silent on the happy path: - `sendDiscoveryBroadcast` logs its broadcasts at debug instead of info, matching the refresh-ping convention established by kpsuperplane#141. - `tryAddDevice` now logs `Updating accessory:` at info only when the display name actually changed (rename path); otherwise it emits the message at debug. Before this, every already-registered device would log a line on every rediscovery even though nothing changed. No behavioural change to the state-tracking itself — just log-level hygiene so the periodic discovery doesn't become a log firehose. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Owner
|
Thank you! |
kpsuperplane
added a commit
that referenced
this pull request
Apr 27, 2026
Documents the reportOffline feature and bundled pilot-state bug fixes from dhananjaysathe's contribution. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in
reportOfflineflag (default:false) that marks bulbs as "Not Responding" in HomeKit once they missofflineThresholdconsecutivegetPilotreplies, rather than silently replaying the last known cached state. Reachability is cleared on any successfulgetPilotreply or anyregistration/getSystemConfigresponse from the same MAC, so bulbs that come back online recover immediately.Leaves existing behaviour unchanged when the flag is left at its default.
Background
Today, when a bulb stops responding to UDP
getPilotrequests,src/accessories/WizLight/pilot.ts:152-158(and the same pattern inWizSocket/pilot.ts:72-78) returns the cached pilot:The original rationale (#48, 2021) was that HAP couldn't surface "Not Responding" at the time — but HomeKit does now honour an
Errorreturned from a characteristicgetcallback by showing the tile as Not Responding, which is what users expect. The error plumbing that #41 already threaded throughutil/network.tsmakes surfacing this trivial.What this changes
src/util/reachability.ts— tiny pure module tracking consecutive miss counts per MAC.recordHit/recordMiss/isOffline(mac, threshold).WizLight/pilot.ts+WizSocket/pilot.ts: ongetPilottimeout, record a miss. IfreportOfflineis on and the miss count hitsofflineThreshold, surface the error up the existingonErrorcallback instead of returning cached state. On any successful reply, clear the miss count.util/network.ts: on any incomingregistrationorgetSystemConfigreply, clear the miss count for that MAC (bulb is demonstrably alive).wiz.ts::initRefreshInterval: whenrefreshInterval > 0, also re-broadcast discovery on each tick so bulbs returning to the network (or acquiring a new DHCP lease) re-announce themselves — otherwisedevice.ipcan stay stale indefinitely. Refresh-loop failures log atdebuginstead oferrorto avoid spamming the log once per tick per offline bulb (matches the convention established by Change log level of pings to debug #141).config.schema.json+README.md: newreportOffline: boolean(defaultfalse) andofflineThreshold: integer(default3).Pairs naturally with the
refreshIntervalpolling added in #128 — background pings detect unreachable bulbs without waiting for HomeKit to read them.Log hygiene
Because discovery now re-broadcasts every tick,
sendDiscoveryBroadcastwas also downgraded todebug. Similarly,tryAddDevice's "Updating accessory:" line now logs atinfoonly when the display name actually changed; otherwisedebug. Without these, a typical 7-bulb / 30s-refresh setup would produce ~22 info-level lines per minute from our changes alone. Same rationale as #141.Bundled defensive fixes in the same code region
Two narrow
undefined-handling fixes inpilot.tsthat live in the exact lines this PR touches and address recurring issues:undefinedfields from thegetPilotreply before merging intocachedPilot. Firmware replies for certain scenes (notablysceneId: 14, "nightlight") omitdimmingentirely, and a naked spread clobbered the state-based default withundefined, producingNaNBrightness in HomeKit. Addresses NaN error from brightness #96, NaN Error Isolated to "sceneId":14 #101 (MoTechnicalities' root-cause), brightness warning #143, Characteristic Warning for Wiz Plugin #159.pilotToColor()falls back to a neutral-white reading when passed an empty cache entry. Prevents the "Cannot read properties of undefined (reading 'temp')" crash when a temperature / color set handler runs after a timeout wiped the cache. Addresses "Unhandled error thrown inside write handler for characteristic: Cannot read properties of undefined (reading 'temp')" when controlling Wiz bulb #145.Issues addressed
Primary (silent-stale-state pattern): #135, #140, #105, #87, #97, #160, #167
Bundled (NaN / undefined-temp crashes in the same code region): #96, #101, #143, #145, #159
Compatibility
Test plan
npm run buildclean (no TS errors on Node 25).reportOffline: true,refreshInterval: 30.offlineThreshold × refreshInterval(default 90s); powering them back on restores state on the next refresh tick.reportOffline: false(default), behaviour is byte-for-byte the same as before — confirmed by diffing log output against pre-PR build.🤖 Generated with Claude Code