Skip to content

anthemav: add connect timeout so setup fails fast instead of blocking bootstrap - #181401

Open
luck-y13 wants to merge 3 commits into
home-assistant:devfrom
luck-y13:fix/anthemav-connect-timeout
Open

anthemav: add connect timeout so setup fails fast instead of blocking bootstrap#181401
luck-y13 wants to merge 3 commits into
home-assistant:devfrom
luck-y13:fix/anthemav-connect-timeout

Conversation

@luck-y13

@luck-y13 luck-y13 commented Sep 5, 2026

Copy link
Copy Markdown

Proposed change

anthemav.Connection.create() retries the initial TCP connection
internally (its own exponential backoff, capped at 300s between attempts)
and only returns once it succeeds — it never raises OSError on its own
when the receiver is unreachable or powered off. Nothing in
async_setup_entry bounds that wait today, so setup blocks indefinitely
whenever the receiver isn't reachable.

At startup this is worse than just a slow boot for this one entry: the only
thing that currently stops it is Home Assistant's own global bootstrap
stage-2 timeout (~5 minutes), and when that fires it force-cancels whatever
other config entries are still mid-setup alongside it too. I ran into this
directly — an Anthem receiver that's normally powered off caused this
integration to block boot for the full 5 minutes, and when the global
timeout finally fired it collaterally killed a separate, unrelated
integration (a water-leak monitor) that was still finishing its own setup
at that moment, with no error logged against that integration individually
— only anthemav's own cancellation is logged, since from bootstrap's
perspective it's the one entry it was still explicitly waiting on.

This wraps the connection attempt in asyncio.timeout(CONNECT_TIMEOUT_SECONDS)
and treats TimeoutError the same as the existing OSError/DeviceError
handling: raise ConfigEntryNotReady so Home Assistant's normal per-entry
retry/backoff takes over, instead of blocking bootstrap. This is the same
shape already used by several other integrations with a similar
"connect once during setup" pattern — matter, cambridge_audio,
music_assistant, zwave_js, hue, lutron_caseta, and others all wrap
their initial connect in asyncio.timeout(...) and raise
ConfigEntryNotReady on TimeoutError.

10s was chosen as generous for a LAN TCP connect while not adding
noticeable delay to a normal boot where the receiver is reachable.

Type of change

  • Bugfix (non-breaking change which fixes an issue)

Additional information

  • This PR is related to issue: none filed yet — happy to open one if useful
  • No breaking change: behavior is identical when the receiver is reachable;
    when it isn't, setup now resolves to SETUP_RETRY within ~10s instead of
    hanging (and, at boot, instead of consuming up to 5 minutes of the global
    bootstrap timeout).

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally. Update: got a working
    environment after all (see comments below) — pytest tests/components/anthemav/ -v passes all 13 tests, including the new
    test_config_entry_not_ready_when_connect_hangs. That run was still on
    Windows rather than Linux, with a documented set of OS-specific
    workarounds (none touching test/implementation logic) — see the
    comment thread for exact commands and caveats. Recommend a Linux/CI
    confirmation too before merge.
  • Local tests pass. Your PR cannot be merged unless tests pass — see
    above.
  • There is no commented out code in this PR.
  • Tests have been added to verify the new code works
    (test_config_entry_not_ready_when_connect_hangs, alongside the
    existing test_config_entry_not_ready_when_oserror).
  • I have reviewed two other open pull requests in this repository. Not
    done
    — will do before/while this is in review if that's expected.

🤖 Generated with Claude Code

anthemav.Connection.create() retries the initial TCP connection
internally (with its own exponential backoff, capped at 300s between
attempts) and only returns once it succeeds, so it never raises OSError
on its own when the receiver is unreachable/powered off. Nothing bounds
that wait in async_setup_entry today, so setup blocks indefinitely.

At startup this is worse than just a slow boot for this one entry: the
only thing that ever stops it is Home Assistant's own global bootstrap
stage-2 timeout (~5 minutes), and when that fires it force-cancels
whatever other config entries are still mid-setup alongside it too —
so one anthemav receiver being off at boot can take down unrelated
integrations that would otherwise have set up fine, with no error
logged against them individually (only anthemav's own cancellation is
logged, from bootstrap's perspective it's the one entry it was still
explicitly waiting on).

Wrap the connection attempt in asyncio.timeout(CONNECT_TIMEOUT_SECONDS)
and treat TimeoutError the same as OSError/DeviceError: raise
ConfigEntryNotReady so Home Assistant's normal per-entry retry/backoff
takes over instead. This is the same pattern already used by several
other integrations with a similar "connect once during setup" shape
(matter, cambridge_audio, music_assistant, zwave_js, hue,
lutron_caseta, ...).

10s was chosen as generous for a LAN TCP connect while still well
inside a boot that won't be noticeably slower when the receiver is
reachable.

Added a test (test_config_entry_not_ready_when_connect_hangs) covering
the new timeout path, alongside the existing
test_config_entry_not_ready_when_oserror.
Copilot AI balanced review requested due to automatic review settings September 5, 2026 20:13

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @luck-y13

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant

home-assistant Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant

home-assistant Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Hey there @Hyralex, mind taking a look at this pull request as it has been labeled with an integration (anthemav) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of anthemav can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign anthemav Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new AsyncMock side effect does not actually block, so the timeout test fails to exercise its intended path, and the PR template is incomplete.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a bounded connection timeout so unreachable Anthem receivers defer setup without blocking bootstrap.

Changes:

  • Adds a 10-second connection timeout.
  • Converts timeout failures into ConfigEntryNotReady.
  • Adds timeout-path test coverage.
File summaries
File Description
homeassistant/components/anthemav/__init__.py Applies and handles the connection timeout.
homeassistant/components/anthemav/const.py Defines the timeout duration.
tests/components/anthemav/test_init.py Tests setup retry behavior.
Review details

Suppressed comments (1)

tests/components/anthemav/test_init.py:95

  • Use an async side effect that is actually awaited so this test reaches the timeout path. Because Connection.create is patched as an AsyncMock, this synchronous lambda returns the sleep() coroutine as its result; the mock await therefore completes immediately and setup fails at avr.protocol instead of timing out.
            side_effect=lambda *args, **kwargs: asyncio.sleep(3600),
  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +49 to +55
# anthemav.Connection.create() retries the initial connection
# internally with its own backoff and only returns once it
# succeeds, so it will not raise OSError on its own when the
# receiver is unreachable. Bound the attempt so an unreachable
# receiver fails fast into the normal ConfigEntryNotReady retry
# path, rather than blocking setup (and, at startup, Home
# Assistant's bootstrap) for as long as the receiver stays off.
Comment on lines +10 to +16
# anthemav.Connection.create() retries its initial connection attempt
# internally (with exponential backoff) and only returns once it succeeds,
# so it does not fail on its own when the receiver is unreachable. Bound it
# here instead of relying on Home Assistant's global bootstrap timeout,
# which would otherwise let one unreachable-at-boot receiver block startup
# for minutes and can collaterally cancel other integrations still setting
# up alongside it.
Comment thread tests/components/anthemav/test_init.py Outdated
Comment on lines +79 to +87
"""Test setup fails fast (instead of hanging) when the AVR never connects.

anthemav.Connection.create() retries its initial connection internally
and only returns once it succeeds, so it never raises OSError on its
own when the receiver is unreachable — nothing bounds that wait except
our own timeout. Simulate that by having the mocked create() hang
indefinitely, and confirm setup still resolves to SETUP_RETRY rather
than blocking forever.
"""
The side_effect was a plain lambda returning asyncio.sleep(3600) —
since AsyncMock calls that synchronously and awaits its return value,
the sleep() coroutine was returned but never awaited, so the mock
resolved immediately without actually exercising the timeout path.
Use an async function instead so the sleep is genuinely awaited.

Verified in isolation (see PR comment) that the original version
returns instantly while the fixed version now correctly triggers
asyncio.timeout()'s TimeoutError.
Copilot AI review requested due to automatic review settings September 5, 2026 20:33
The same explanation of why CONNECT_TIMEOUT_SECONDS exists was
repeated across the implementation, the constant, and the test
docstring. Keep it in one place (the constant) and just point there
from the call site; trim the test docstring to its behavior contract.
@luck-y13

luck-y13 commented Sep 5, 2026

Copy link
Copy Markdown
Author

Addressed Copilot's review:

  1. Real bug it caught: the test's side_effect was a plain lambda returning asyncio.sleep(3600) — since AsyncMock calls that synchronously and awaits its return value, the sleep coroutine was returned but never actually awaited, so the mock resolved instantly without ever exercising the timeout path. Fixed by using an actual async def side effect instead.

    I can't run this repo's test suite locally (Windows dev machine — hit both the fcntl stdlib gap and then a zeroconf native-extension DLL blocked by this machine's Application Control policy once I stubbed around the first one), so I couldn't have caught this the way CI would. I verified the specific behavior in isolation instead (no homeassistant import needed) — before the fix, the mock returns immediately with an unawaited coroutine object as its result; after the fix, asyncio.timeout() correctly raises TimeoutError. Happy to paste that repro here if useful, or defer entirely to CI's real run.

  2. Style nits (3 comments): the same rationale for the timeout was duplicated across __init__.py, const.py, and the test docstring. Trimmed to keep it in one place (the constant) and reference it from the call site.

Pushed both as separate commits so the history shows what changed and why.

@luck-y13
luck-y13 marked this pull request as ready for review September 5, 2026 20:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The required pull-request template is incomplete, and several added comments should be condensed.

Review details

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

homeassistant/components/anthemav/init.py:65

  • Restore the complete pull-request template before merge. The description omits multiple unchecked type, additional-information, checklist, and device-integration items, which must remain present even when they do not apply.

homeassistant/components/anthemav/init.py:49

  • Condense this explanation to the single non-obvious constraint. The seven-line block repeats the constant comment and PR description for one timeout operation, making the implementation harder to scan.
        # See CONNECT_TIMEOUT_SECONDS for why this needs a timeout.

homeassistant/components/anthemav/const.py:10

  • Condense this constant comment to one line. The detailed bootstrap narrative is duplicated at the call site and will be harder to keep synchronized.
# anthemav.Connection.create() retries internally and only returns once

tests/components/anthemav/test_init.py:79

  • Shorten this test documentation to a one-line behavior description. The multi-paragraph docstring and helper docstring narrate the mock implementation rather than adding a non-obvious testing constraint.
    """Test setup fails fast (instead of hanging) when the AVR never connects."""
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 5, 2026 20:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The PR description omits required template fields and checklist items.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

homeassistant/components/anthemav/init.py:50

  • Restore the complete pull request template before merging. The description removes the unchecked “Type of change” options, required “Additional information” fields, and multiple checklist items; repository instructions require all template sections and checkboxes to remain present even when unchecked.
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@blues-sechseck blues-sechseck left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read through this while looking at how other integrations bound their setup connect, and checked the premise against anthemav 1.4.2 itself. It holds: Connection.create() calls conn.reconnect(), which loops until it succeeds and backs off to 300 s between attempts, and only re-raises OSError when auto_reconnect is false — which it isn't here, since the integration takes the default. So nothing bounds that wait today and a timeout is the right shape.

Two things I noticed, one worth changing before merge.

The new message is attached to DeviceError too, where it isn't true. wait_for_device_initialised() raises DeviceError, converting its own timeout internally — so adding TimeoutError to the tuple doesn't change that path, but the message does. That case means the TCP connection succeeded and the receiver simply didn't report model and MAC within DEVICE_TIMEOUT_SECONDS, and it now logs "Unable to connect to Anthem AVR at host:port", pointing the user at the network for a problem that isn't there. Either split it:

    except TimeoutError as err:
        raise ConfigEntryNotReady(
            f"Timed out connecting to Anthem AVR at "
            f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
        ) from err
    except (OSError, DeviceError) as err:
        raise ConfigEntryNotReady from err

or word the shared message so it is true for both, e.g. "Anthem AVR at host:port is not ready".

The timed-out connection is never closed. Optional, and not introduced here. When the timeout fires, avr was never assigned, so there is no handle to close(). In the case you are targeting this costs nothing — the receiver is off, create_connection keeps failing, nothing is open. But if it connects just after the deadline, the Connection is left with a live transport and auto_reconnect=True; its connection_lost callback reconnects forever with nobody holding a reference, and its update_callback still fires async_dispatcher_send for an entry that failed setup. Each ConfigEntryNotReady retry can add one. The DeviceError path has had the same shape all along, so I would not hold the PR for it — but Connection.create() does take auto_reconnect, if you want to build the connection yourself and keep the handle.

For what it is worth, I ran the tests: on this branch pytest tests/components/anthemav gives 13 passed, on unmodified dev 12. Both runs show the same 9 errors, which are a translation-lookup artefact of my checkout and unrelated to the change. The new test does exercise the hang rather than an early raise, and it follows the patching style already used in that file.

@luck-y13

luck-y13 commented Sep 6, 2026

Copy link
Copy Markdown
Author

Follow-up: I found a way to actually execute this repo's test suite (a sandboxed environment, which — despite my intent — turned out to still be Windows rather than Linux, so this isn't equivalent to a clean CI run, but it is a real, unmocked execution of the actual test code rather than manual tracing).

pytest tests/components/anthemav/ -v --force-enable-socket
...
13 passed in 1.04s

All 13 tests in tests/components/anthemav/ pass, including the new test_config_entry_not_ready_when_connect_hangs, with zero changes to test or implementation logic to get there. What it took (for anyone else trying this on Windows):

  • Stub fcntl and resource modules (Unix-only stdlib modules imported at module scope by files this test path never actually calls into).
  • --force-enable-socket: Windows' asyncio event loop implementations build their internal self-pipe wakeup with a real AF_INET socket.socketpair(), not AF_UNIX like Linux/macOS — tests/conftest.py's pytest_socket.disable_socket(allow_unix_socket=True) blocks that on Windows for literally every async test regardless of what the test does. This is a Windows/asyncio limitation, not anything related to this PR.
  • Two OS-agnostic gaps unrelated to this change, needed on any fresh checkout: pip install paho-mqtt aiohasupervisor (transitive test deps tests/conftest.py needs regardless of component), and python -m script.translations develop --integration anthemav / --integration media_player (compiled translation files aren't checked in).

I'd still expect (and recommend) a real Linux/CI confirmation before merge, but wanted to close the loop on the "couldn't verify tests pass" caveat from my original submission — this is as close as I could get to that on the machine available to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants