Repository navigation
Fix Home Assistant connection errors: dropped calls and an unreachable HA at startup - #140
Merged
Merged
Conversation
…e HA at startup
A call in flight when the socket drops was rejected by the library with a
whole failed result ({type: "result", success: false, error: {code: 3}}),
which translateError passed through, so it was logged as an unexpected
failure and sent to Sentry as "Object captured as exception". It is now
unwrapped and treated as unreachable (string codes inside stay refusals).
With setupRetry -1 the library retried every second forever, silently, so
an unreachable Home Assistant at startup never settled: no log, no Sentry
report, and the login and devices checks never ran. Each attempt is now a
single, time-limited try; a failure logs home.connect_failed once, startup
reports it once, and Pixel keeps retrying with backoff (1 s doubling up to
60 s), running the checks once it connects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines
+285
to
292
| if (this.#closed) return; | ||
| if (isUnreachable(error)) { | ||
| this.#retryLater(error); | ||
| return; | ||
| } | ||
| this.#logger.error( | ||
| { event: "home.connect_failed", why: describeFailure(error) }, | ||
| "couldn't connect to Home Assistant", |
There was a problem hiding this comment.
Bug: The connected promise never settles if connection retries fail with a terminal error, causing the startup sequence awaiting backend.connected to hang indefinitely.
Severity: MEDIUM
Suggested Fix
Ensure connected settles or rejects when the backend transitions to off, or handle non-retryable errors during connection retries by rejecting the pending connected promise.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: src/adapters/home-assistant/backend.ts#L285-L292
Potential issue: The `connected` promise is only settled upon a successful connection.
If the initial connection attempt fails due to an unreachable Home Assistant instance
and a subsequent retry encounters a non-retryable error (such as an invalid token), the
backend transitions to `off` via `#turnOff()`, but `connected` never resolves or
rejects. Because the startup sequence in `index.ts` awaits `backend.connected`,
execution hangs, preventing the secondary `home.check()` and missing device checks from
ever running.
Did we get this right? 👍 / 👎 to inform future reviews.
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.
What was wrong
Two problems in
src/adapters/home-assistant/backend.ts, both checked againsthome-assistant-js-websocket9.7.0 (connection.js,socket.js).1. A call cut off by a dropped connection was reported as a bug. When the socket closes, the library rejects every call still in flight with a whole failed result:
{ type: "result", success: false, error: { code: 3 /* ERR_CONNECTION_LOST */, message: "Connection lost" } }(_handleClose→messages.error(...)).translateErroronly knew bare numbers and bare{ code: string }, so it passed this object through unchanged. The coreHomethen loggedhome.failed("unexpected Home Assistant failure") at error level, and Sentry got "Object captured as exception". Example: someone runs/ha setjust as the Nabu Casa tunnel blips.2. If Home Assistant was unreachable at startup, nobody heard about it. With
setupRetry: -1,createSocketretries every second forever, logs nothing, and rejects only when the token is refused. Sosettlednever resolved,home.check()(with its one-time Sentry report) and thehome.devices_missingwarning never ran, and nothing was logged. The doc comment ("Settles once the first attempt has either connected or given up") was wrong.What changed
translateError(anddescribeFailure, and the refused-token check) now unwrap a failed result first. A numeric code inside (the library's own connection codes) meansHomeUnavailableError. A string code inside (Home Assistant's own) meansHomeRequestError, which keeps the code for the logs. Anything else still goes through unchanged. Messages never include the token, the address or HA's text.setupRetry: 0), limited to 15 s overall. The library only times out opening the socket, so this limit also covers a login that never gets an answer. A connection that turns up after the limit is closed. If HA can't be reached (ERR_CANNOT_CONNECT,ERR_CONNECTION_LOST, or the time limit runs out), Pixel logshome.connect_failedat warn once, then retries in the background: 1 s, doubling, up to 60 s, with each later attempt logged only at debug. When it gets through it logshome.ready. A refused token or a bad address still turns it off, as before. Once connected, the library handles reconnects after a drop, as before.close()cancels any pending retry.settlednow settles within about 15 s whatever happens. The newconnectedpromise settles the first time it connects.src/index.ts: if the startup check saysconnecting, Pixel reports once to Sentry ("Couldn't connect to Home Assistant at startup. Still trying."). Whenconnectedsettles it runshome.check()again, then the missing-devices check. That way the admin-token warning andhome.devices_missingstill happen once HA comes back.docs/home-assistant.mdand to the runbook's "Home Assistant is unreachable" entry. Added aFixedentry to the changelog.src/testing/fake-home-assistant.ts: newhangService(key), so a test can leave a call in flight and then drop the connection.How it was tested
New cases in
backend.test.ts, all against the fake or a localwsserver, never a real HA:translateErrorwith the connection-lost result, a failed result carrying an HA string code (the code is kept, the message text is not shown), and odd or successful results (passed through).HomeUnavailableError.settledresolves withconnecting, there is exactly onehome.connect_failedwarning, calls fail at once, retries loghome.connect_retry, and it connects (connected,home.ready) once the server starts.{ first: 10, max: 40 }.attemptTimeoutMs(why: ConnectTimeout).close()stops the retries. A bad address goes off (error level) and does not retry.The old test "keeps trying… settled stays pending" was replaced, because that pending
settledwas the bug.just checkpasses: 1996 tests, coverage above every threshold.Not touched: rule-14 files.
core/home.tsis unchanged.Reviewer note: an attempt that times out while the socket is still open, still waiting for a login answer, keeps that socket until the server or a proxy closes it. The library doesn't give access to the raw socket. A real Home Assistant doesn't behave this way.
🤖 Generated with Claude Code