Skip to content

Offline-startup API - #9

Open
theharshl wants to merge 15 commits into
hikirsch:mainfrom
theharshl:up/l3
Open

Offline-startup API#9
theharshl wants to merge 15 commits into
hikirsch:mainfrom
theharshl:up/l3

Conversation

@theharshl

Copy link
Copy Markdown

Groundwork so a consumer can construct a client and set up entities without the amplifier being reachable:

  • build_client factory, with async_get_client rewritten on top of it — construction is now separable from connection.
  • async_start no longer raises when the device is unreachable; it returns and lets a single-handle reconnect loop retry with exponential backoff, logging each attempt.
  • Model-key and cached-name accessors, so a consumer can persist which model it saw and rebuild the right client later without probing.
  • Commands issued while disconnected raise HtdConnectionError instead of AttributeError, and connection state is broadcast to subscribers.

Existing callers are unaffected — async_get_client keeps its signature and behavior.

Tests: 200 passing.


Part of the fork-upstreaming stack — see hikirsch/htd-home-assistant#34. Based on L2; until that merges the diff above includes its changes too. Please merge with a plain merge commit so the rest of the stack stays intact.

kingfetty and others added 15 commits March 27, 2026 00:43
…and Lync clients, data models, constants, and tests.
- Update MCA bass/treble/balance range to -12..12
- Enforce step size of 4 for bass/treble and 6 for balance
- Remove scaling logic from client and HA integration
- Add unit tests for MCA audio control logic and rounding
- Bump version to 0.0.26
…ance declaration

SET_ECHO_COMMAND_CODE and QUERY_ID_CODE are already defined in constants.py — drop the defensive hasattr ternaries in lync_client.py. Also add the missing @AbstractMethod declaration for async_set_balance in base_client.py alongside async_set_bass and async_set_treble.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Populate _zone_names on every ZONE_NAME_RECEIVE_COMMAND regardless of
whether zone_data exists yet. Expose get_zone_name(zone) -> str | None
mirroring get_source_name(). Also initialize _zone_names in async_connect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… cache tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
async_get_model_info() previously probed once with no retry, so a bad
read (partial data, or a gateway that resets on DTR toggle when a USB
serial adapter opens the port) left model_info as None, which crashed
async_get_client() with a bare TypeError on model_info["kind"].

Wires the existing retry_attempts into the model probe, adds a settle
delay after opening the serial port before the first write, and raises
a clear ValueError instead of crashing when detection still fails.

Fixes theharshl/htd-home-assistant#6
The model probe read the reply with a single StreamReader.read(), which
returns as soon as any bytes are buffered — a cheap USB-serial adapter
delivering a reply across several USB packets was misread as a failed
probe. Reads now accumulate until the reply matches a known model, the
line goes quiet, or an overall deadline expires; a silent device no
longer hangs the probe forever.

Probe retries also re-opened the serial port on every attempt, and each
open can toggle DTR and reset the gateway — so retries kept resetting
the device they were probing. The probe now opens the connection once,
pays the settle delay once, and retries on the open connection. The
persistent connection now waits out the same settle delay before its
first refresh write, which was previously sent immediately after the
port opened and could be lost to the same reset.

Replaces utils.async_send_command with async_open_connection +
async_read_response; renames MODEL_PROBE_SETTLE_DELAY to
SERIAL_SETTLE_DELAY now that it also guards the persistent connection.

Reported by @steve28 in theharshl/htd-home-assistant#19 (MCA-66 over
USB-serial).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A checksum mismatch means the parsed zone/command/length can't be
trusted -- it may be a coincidental match on misaligned or corrupted
bytes -- so skipping the full presumed frame length left the parser
permanently desynced after a single dropped/corrupted byte from a
flaky USB-serial adapter. Resync by header length only, matching the
existing unknown-command recovery path, so one bad frame no longer
cascades into a stream of "Bad sync buffer" / "Invalid command value"
errors during normal zone control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Give each SUPPORTED_MODELS entry a persistable "key" and add
get_model_info(key) so a caller can rebuild the correct client class
from storage alone, without probing the device. Add get_source_names()
and get_zone_names() so only controller-confirmed names (never
placeholders) are safe to persist across restarts.
…safe disconnect

async_start() connects once and, on failure, hands off to a while-loop reconnect task
guarded by a single handle (_ensure_reconnect_task) instead of the old self-rescheduling
_async_reconnect, which left a window where connection_lost could see a stale, not-done
task handle and fail to start a new attempt. disconnect() is now safe on a client that
never connected and cancels any in-flight retry task.

Updates test_reconnect_logic in test_base_client_coverage.py, which asserted the old
self-rescheduling behavior (asyncio.create_task called from within _async_reconnect); it
now asserts the loop's observable contract (two connect attempts, then return) instead.
…cted, and broadcast connection state

- _send_cmd, HtdMcaClient.refresh, and HtdLyncClient.refresh now raise
  HtdConnectionError when the connection is None or not connected, instead of
  the AttributeError that came from writing to a None transport.
- The heartbeat loop exits quietly on HtdConnectionError instead of dying with
  an unretrieved task exception.
- connection_made and connection_lost each broadcast(None) so subscribed
  entities (should_poll = False) repaint on connect/disconnect transitions.
- Fixed test fixtures/tests that set _connection but never _connected, which
  the new guard exposed as tests implicitly assuming a connected state:
  test_send_and_validate_success/timeout, htd_instance (test_client.py),
  lync_client (test_lync_client_coverage.py), mca_client (test_mca_client_coverage.py).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants