Repository navigation
fix: one status request per polling loop, and handle HTTP 429 - #37
shejnowicz wants to merge 3 commits into
Conversation
Starting a zone returned HTTP 429 (Hunter rate-limits the cloud per account) and only an app restart cleared it. Three separate causes, each fixed here. - updateZoneStatus claimed to fetch "only once per loop", but app.js starts every device's update in parallel and the batch marker was assigned only AFTER the await — so each of the six zone devices issued its own request. One in-flight request is now shared per controller per loop. Keying by controller+API key rather than a single shared field also fixes a latent bug for multi-controller accounts, where every zone read whichever controller answered last. - stopPolling cleared a this.homey.setInterval handle with the GLOBAL clearTimeout. startPolling runs on app init, on either settings change and from two flow cards, so a missed cancel leaves the previous loop running — which is why restarting the app fixed the 429. - The legacy statusschedule/setzone path had no 429 handling at all: the bare axios wrapper rethrew, the raw error reached the user and the zone device went unavailable. It now shares one RateLimit gate with a Retry-After cool-off (floored at 60s), so a penalty triggered by polling also holds back commands, and the user gets "rate_limited:<seconds>" instead of a stack trace. The GraphQL sensor client already did this. Also removes a stray `sure;` in updateZoneStatus's catch block: referencing that undefined identifier threw a ReferenceError, so setUnavailable never ran and the real error was replaced by the reference failure. Tests: six parallel zones collapse to one call (verified failing without the fix), a new loop refetches, separate controllers stay separate, a failed batch is not reused, a 429 blocks further calls during the cool-off, and the Retry-After parsing floors both header shapes.
Await capability updates before clearing an unavailable state after transient DNS or API errors. Cover all six zones, missing relays, and asynchronous writes. Refs shejnowicz/vishome#218.
fix: recover zone availability after transient API failures
|
Heads-up on scope: this branch gained a second, related fix — commits Zone availability now recovers after transient API failures. Previously a DNS/network/API error marked every affected zone unavailable via Also awaits the previously fire-and-forget Covered by 4 new node tests (six-zone recovery, missing relay, write-ordering, failed-write fail-closed) on top of the existing 6; Happy to split this into a separate PR if you prefer them reviewed independently — they stack cleanly. |
Starting a zone returned HTTP 429 from the Hydrawise cloud, and only restarting the app cleared it. Investigating that turned up three separate causes. Each is fixed here, with tests.
1. The app spends one request per ZONE per loop, not one per loop
Zone.updateZoneStatuscarries the comment "batch to make sure that data from Hydrawise is fetched only once per loop", but the guard cannot deliver it:app.jsstarts every device's update in parallel and awaits them withPromise.allSettled, so each device evaluates the guard before the first response lands, sees the marker unchanged, and issues its own request. On an account with six zones that is six simultaneous calls a minute — which is what tripped the rate limit.One in-flight request is now shared per controller per loop. Keying by controller + API key rather than one shared
this.dataalso fixes a latent bug for multi-controller accounts, where every zone previously read whichever controller answered last.2.
stopPollingcould not cancel the loop it startedstartPollingcreates the timer withthis.homey.setInterval, butstopPollingcleared it with the globalclearTimeoutinstead ofthis.homey.clearInterval.startPollingruns on app init, on apollingEnabledchange, on apollingIntervalchange and from both flow cards, so a missed cancel leaves the previous loop running alongside the new one. That is why restarting the app cleared the 429 — the restart killed the accumulated timers.3. The legacy path had no 429 handling at all
lib/httpService.jsis a bare axios wrapper whose interceptor only rethrows, so a 429 fromstatusschedule.phporsetzone.phpreached the user raw and the zone device was marked unavailable. Legacy calls now pass through one sharedRateLimitgate honouringRetry-After(floored at 60 s), so a penalty incurred while polling also holds back zone commands — they share the account's quota, so they must share the penalty — and the caller getsrate_limited:<seconds>instead of a stack trace.Also
Removes a stray
sure;at the top ofupdateZoneStatus's catch block. Referencing that undefined identifier throws aReferenceError, sosetUnavailable(ex)never ran and the real error was replaced by the reference failure.Tests
Six new cases in
test/zone-ratelimit.test.js(node --test, no new dependencies): six parallel zones collapse to one call; a new loop refetches; separate controllers stay separate; a failed batch is not reused by the next loop; a 429 blocks further calls during the cool-off; andRetry-Afterparsing floors both the seconds and HTTP-date forms. The batching test was verified to fail against the current code before the fix.No version bump — left to the maintainer, matching how releases are cut in this repo.
homey app validate --level publishpasses.