Skip to content

Release v5.4.0 - #36

Merged
tis24dev merged 11 commits into
mainfrom
dev
Jun 26, 2026
Merged

Release v5.4.0#36
tis24dev merged 11 commits into
mainfrom
dev

Conversation

@tis24dev

@tis24dev tis24dev commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Automated release PR for v5.4.0.

Summary by CodeRabbit

  • New Features

    • Added a new “Refresh now” service to trigger an immediate cloud update for all configured devices.
    • Expanded documentation to highlight 2FA login support and multilingual availability.
  • Bug Fixes

    • Improved device connectivity handling for more reliable online/offline status.
    • Climate controls now avoid guessing unsupported heating/cooling and fan modes.
    • Sensitive device details are now better redacted in logs.
  • Chores

    • Updated the integration version.

Greptile Summary

This release adds a domain-wide addhon.refresh service, fixes stale REST DISCONNECTED events clobbering live MQTT-connected devices, upgrades the MQTT subscription tracker from a binary flag to a per-topic set (H1 isolation), and replaces guessed HVAC/fan modes with explicit None returns when a mode is unmapped or unadvertised.

  • New addhon.refresh service: registers once per domain, iterates all loaded coordinators via asyncio.gather(return_exceptions=True), isolates per-entry failures, and is cleaned up on last unload alongside the existing log-level services.
  • Stale-disconnect fix (appliance.py + helpers.py): mark_realtime_seen() records a cloud-stamped timestamp and a monotonic receipt time; load_attributes compares these against the REST lastConnEvent to prevent a frozen DISCONNECTED from knocking a live device offline, while still honoring a genuinely newer disconnect and expiring the protection after 300 s.
  • Per-topic MQTT subscription set (mqtt.py): replaces _subscribed: bool with _subscribed_topics_set: set[str]; _subscribe_missing() skips already-subscribed topics, isolates per-topic failures, and the watchdog escalates to a full rebuild only on a total blackout (nothing subscribed at all), never on a single chronically-broken appliance topic.

Confidence Score: 5/5

Safe to merge — all three core changes (stale-disconnect fix, per-topic MQTT set, new refresh service) are covered by dedicated tests and the previous monotonic/exception-handling concerns are resolved.

The stale-disconnect reconciliation uses monotonic() for TTL (immune to DST/NTP jumps) and cloud-stamped timestamps for ordering (skew-free). The MQTT subscription redesign swallows per-topic exceptions inside _subscribe_missing() and only escalates to a rebuild on a total blackout, which is well-tested and intentional. The refresh service isolates per-entry failures with asyncio.gather(return_exceptions=True) and never re-raises to the automation caller. No issues were found that would cause incorrect behavior at runtime.

No files require special attention.

Important Files Changed

Filename Overview
custom_components/addhon/init.py Adds the domain-wide addhon.refresh service with per-entry failure isolation; updates the unload cleanup and early-return guard to include the new service; switches legacy-cleanup INFO logs to redacted IDs.
custom_components/addhon/client/engine/appliance.py Adds mark_realtime_seen() / mark_realtime_disconnected() for cloud-vs-cloud timestamp ordering; TTL freshness uses monotonic() (fixes the previous P2 about datetime.now() DST sensitivity); load_attributes now preserves liveness when realtime evidence is newer than a REST disconnect.
custom_components/addhon/client/helpers.py New parse_cloud_timestamp() helper: total (never raises), accepts epoch-ms or ISO8601 with variable fractional digits, always returns tz-aware UTC; guards against OverflowError on arbitrary-precision ints from json.loads.
custom_components/addhon/client/transport/mqtt.py Replaces _subscribed: bool with _subscribed_topics_set: set[str]; adds _subscribe_missing() with per-topic exception isolation; watchdog escalates to rebuild only on total blackout; $aws/events/presence/ topics are intercepted and ignored for appliance connectivity; addresses both previous P2 findings.
custom_components/addhon/climate.py hvac_mode now returns `HVACMode
tests/test_refresh_service.py New test file: covers registration idempotency, per-entry failure isolation (async and synchronous raises), None-coordinator skipping, live hass.data enumeration, and source-level wiring guards.
tests/test_engine_appliance_root.py New RealtimeReconciliationTest class: 13 tests covering stale-disconnect protection, TTL expiry, explicit MQTT disconnect clearing liveness, ISO/epoch comparability, and out-of-order timestamp handling.
tests/test_transport_mqtt.py Updated to use _subscribe_missing instead of _subscribe_appliances in test stubs; adds tests for mark_realtime_seen forwarding, session-presence topic isolation, and the per-topic subscription set model.
tests/test_ac_write_path.py New AcClimateReadPathTest class: covers valid/unmapped/unadvertised hvac_mode and fan_mode cases, including missing machMode and the OFF-wins-over-unmapped invariant.

Comments Outside Diff (2)

  1. custom_components/addhon/client/transport/mqtt.py, line 698-702 (link)

    P2 Unexpected subscribe exceptions bypass escalation counter

    _subscribe_missing only catches HonCodedError (which wraps asyncio.TimeoutError). If the underlying concurrent.futures.Future from self.client.subscribe() fails with any other exception (e.g., a closed/bad-state client that raises directly), it escapes _subscribe_missing and is caught by the outer except Exception in the watchdog. That handler adds backoff but does not increment resubscribe_failures, so the escalation to a full rebuild never fires via that counter — the watchdog would retry indefinitely with capped backoff rather than refreshing the AWS token.

    In the old design, attempted_resubscribe = True ensured any exception from the re-subscribe path incremented the counter. The new design relies entirely on the resubscribe_failures inline path, which is only reached when _subscribe_missing returns normally with an empty set — not when it raises. The watchdog comment "Only the REBUILD path … can land here now" is also inaccurate for this edge case.

    In practice, awscrt failures tend to trigger the disconnection lifecycle callbacks first, so _connection would flip to False and a normal reconnect would occur. But the escalation guarantee is technically broken for the pathological case of a connected-but-subscribe-failing client without a disconnection callback.

    Fix in Claude Code Fix in Cursor Fix in Codex

  2. custom_components/addhon/client/engine/appliance.py, line 329-331 (link)

    P2 Naive datetime.now() for wall-clock TTL freshness check

    _last_realtime_local is stored and compared using naive datetime.now() (local wall-clock time). The subtraction in load_attributes (datetime.now() - self._last_realtime_local) is a local-to-local delta, so it works correctly during normal operation. However, a DST "fall back" transition — where the local clock moves backwards by one hour — can cause the delta to appear larger than it actually is, potentially expiring the 300-second TTL prematurely and briefly allowing a stale REST DISCONNECTED to win. datetime.now(timezone.utc) avoids this entirely and has no extra cost. Most HA Docker deployments run in UTC so the risk is low, but the fix is trivial.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code Fix in Cursor Fix in Codex

Reviews (2): Last reviewed commit: "fix: address v5.4.0 review findings (pre..." | Re-trigger Greptile

tis24dev and others added 9 commits June 25, 2026 13:22
Added sections for two-factor authentication and multilingual integration. Removed outdated localization and how it works sections.
The "Added program select", "Added pause switch" and the two legacy
entity-removal INFO logs emitted the appliance nickname / entity_id (whose
object_id is the nickname slug) in clear. INFO is not gated by the debug
toggles, so it always reaches home-assistant.log and gets attached to public
GitHub issues. Log the redacted id (redact_id) instead.

- select.py / switch.py: log redact_id(appliance_id)
- __init__.py: import redact_id; both removal logs use redact_id(unique_id)
- behavioral tests assert the nickname/entity_id never reach the INFO log
  (mutation-proven); relax the brittle source-substring import guard to a regex
hvac_mode/fan_mode coerced an unmapped or out-of-enum raw value into a guessed
COOL/auto, which could fall outside the advertised _attr_hvac_modes /
_attr_fan_modes; HA then logs "X is not a valid option" and ignores it. Return
None (unknown) when the resolved value is unmapped or not in the advertised
list. The full-list fallback keeps the common, healthy-device case unchanged.

- climate.py: hvac_mode -> HVACMode | None, fan_mode None-guards on the
  advertised list
- read-side tests for unmapped / mapped-but-not-advertised values (mutation-proven)
The static guard caught only bare Name/Attribute identity refs, so it missed
the v5.2.0/2026-06-25 leak forms: `data.get("name")` (an ast.Call) at INFO and
a bare `reg_entry.entity_id` in __init__.py (which was not even registered).

- register __init__.py in _FILES, forbidding bare .entity_id / .unique_id in the
  legacy-cleanup logs
- new _identity_call_offender: flag `<x>.get("name"/nick_name/nickName/nickname)`
  and `_get_name(...)` passed to a NON-gated logger call (info/warning/error/
  exception/critical); the same at .debug stays allowed (debug is gated)
- add the camelCase `nickName` attribute to the forbidden entity attrs (_get_name
  reads both nick_name and nickName via getattr)
- two meta-tests pin the new check and its level gate (non-vacuous)
… stale REST lastConnEvent

An appliance was marked unavailable whenever the 60s REST poll's
lastConnEvent.category was DISCONNECTED, even while it was streaming realtime
MQTT appliancestatus updates. The Haier cloud can leave lastConnEvent frozen on
an old DISCONNECTED for hours (observed: ~2.5h) while the appliance is online,
so HA showed the device offline although the hOn app showed it connected
(discussion #34, reproduced with a diagnostics dump + debug log).

Treat realtime MQTT traffic as authoritative connectivity evidence, reconciled
against the REST lastConnEvent by timestamp:

- mark_realtime_seen(): an appliancestatus message marks the appliance connected
  (mirrored onto the raw `available` attribute the connectivity binary_sensor and
  the availability gate read, so it flips at once, not at the next poll) and
  records the cloud message time plus a local receipt time.
- load_attributes reconciliation: a DISCONNECTED lastConnEvent downgrades to
  offline only when the disconnect event is NEWER than the last realtime message
  (cloud-vs-cloud ordering) AND that message is still within a wall-clock
  freshness window (_REALTIME_LIVENESS_TTL = 300s). The freshness bound prevents
  a once-seen realtime time from pinning a silently-dead appliance online forever
  while the cloud keeps returning a frozen-stale disconnect. Missing/unparseable
  timestamps fall back to honoring the DISCONNECTED.
- mark_realtime_disconnected(): a device-scoped MQTT `disconnected` clears the
  liveness marks so a stale REST disconnect cannot resurrect the appliance.
- Topic dispatch: AWS session-presence events ($aws/events/presence/...) are OUR
  client's session, not appliance connectivity, so they neither arm nor clear the
  protection (our client reconnecting/dropping must not pin a dead appliance
  online nor knock a live one offline). Only device-scoped events drive it.
- parse_cloud_timestamp(): defensive, total parser for the two cloud time shapes
  (epoch-ms and ISO8601 with Z); never raises, including on huge ints.

Type-agnostic (the original report was a dryer, the repro a washer). Adds unit
tests for the reconciliation, the freshness/stuck-online guard, the explicit
disconnect, session-presence discrimination, and parser totality.
…poll

Adds a domain-wide service/action `addhon.refresh`, the automation-callable
equivalent of the per-device "Refresh now" button (requested by a user in
discussion #34: "an action, service or button to trigger such polling"). The
button can only be pressed by hand from the UI; the service can be called from
automations, scripts and Developer Tools.

The handler reads hass.data live at call time, collects every loaded entry's
coordinator and asks each for a debounced refresh (async_request_refresh, like
the button), running them concurrently. Per-entry failures are isolated: each
call is wrapped in a coroutine and gathered with return_exceptions=True, so a
synchronous OR awaited raise from one account is logged at warning and never
re-raised to the caller nor aborts the other accounts. The service is global to
the domain: registered once (idempotent guard) and removed on the last unload,
alongside the existing log-level services. No fields, no target.

Adds services.yaml entry, en/it translations, and tests (registration,
idempotency, refresh-all, per-entry isolation including a synchronous raise,
live hass.data read, no-op when no entries). Narrows the pre-existing
test_init_does_not_repoll_on_push source guard to the realtime push region so the
service's legitimate use of async_request_refresh is not a false positive, while
still asserting the push path never re-polls.
… cannot starve the rest (H1)

_subscribe_appliances subscribed appliances sequentially and _subscribe raised
HonCodedError(MQTT_SUBSCRIBE_TIMEOUT) on the first stalled topic, aborting the
loop. Every appliance after the slow one was never subscribed, so a single slow
appliance early in the list permanently starved the rest of realtime push (they
fell back to the possibly-stale REST poll). The binary _subscribed health flag
then stayed False on any single failure, eventually escalating to a full _start()
rebuild that dropped ALL subscriptions and re-hit the same bad topic, periodically
disrupting the healthy appliances too.

Replace the binary _subscribed bool with a set of acked topic strings
(_subscribed_topics_set), giving per-topic isolation and letting the watchdog
distinguish a single bad appliance from a dead connection:

- _subscribe_missing() subscribes only not-yet-subscribed topics and, on a
  per-topic timeout, logs a warning and continues instead of aborting; the failed
  topic is simply retried next watchdog tick while the healthy topics stay
  subscribed. _subscribe_topic() is the extracted one-topic primitive.
- Watchdog health is now "connected AND no missing topics". In the
  connected-but-missing branch it re-subscribes the missing topics, commits the
  set only under the existing connection+generation guard, then: if any topic is
  subscribed (set non-empty) the connection is alive and remaining misses are
  appliance-specific -> reset the outage counters, do NOT escalate; only a total
  blackout (nothing subscribes at all) counts toward _MAX_RESUBSCRIBE_FAILURES and
  escalates to a rebuild, preserving the original "one tick past the cap" timing.
- The set is rebound to an empty set (atomic) wherever the old flag was reset
  (_start, the disconnection/connection-failure lifecycle callbacks); create() and
  the watchdog commit it only when connection and generation still match.

Constant values, the generation-guard pattern, awscrt-thread callback safety and
the not-connected grace path are unchanged. Adds tests for per-topic isolation,
chronic-bad-topic-does-not-rebuild, total-blackout-still-escalates, and
disconnect-mid-subscribe clearing the set.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a refresh service, updates MQTT realtime/subscription handling and appliance liveness reconciliation, changes climate mode reporting to return None for unknown values, and revises documentation, logging, and privacy checks.

Changes

Project docs and release metadata

Layer / File(s) Summary
README and release metadata
.github/FUNDING.yml, README.md, custom_components/addhon/manifest.json
README.md adds 2FA and multilingual feature bullets, removes the Localization/How It Works/Entities/Services sections, shortens Contributing, .github/FUNDING.yml adds tis24dev, and manifest.json bumps the version.

Refresh service

Layer / File(s) Summary
Service contract
custom_components/addhon/const.py, custom_components/addhon/services.yaml, custom_components/addhon/translations/*
SERVICE_REFRESH is added with no fields or target, and its English and Italian labels/descriptions are declared.
Service wiring
custom_components/addhon/__init__.py
SERVICE_REFRESH is registered alongside the existing domain services, _handle_refresh calls async_request_refresh() on loaded coordinators with exception isolation, and unload removes the service.
Refresh service tests
tests/test_refresh_service.py
Tests cover registration idempotence, fan-out to coordinators, error isolation, late-added entries, and source wiring.

Realtime MQTT and appliance liveness

Layer / File(s) Summary
Timestamp parsing and appliance reconciliation
custom_components/addhon/client/helpers.py, custom_components/addhon/client/engine/appliance.py, tests/test_client_str_to_float.py, tests/test_engine_appliance_root.py
parse_cloud_timestamp() normalizes epoch-ms and ISO cloud timestamps to UTC, and appliance availability now compares MQTT realtime evidence against REST DISCONNECTED timestamps and a freshness TTL.
Realtime MQTT events
custom_components/addhon/client/transport/mqtt.py, tests/test_transport_mqtt.py, tests/test_hon_client_realtime.py
appliancestatus now forwards timestamps into mark_realtime_seen(), device disconnected events clear realtime liveness, AWS presence topics are ignored, and the realtime push wiring test narrows its no-repoll guard.
Subscription state and helpers
custom_components/addhon/client/transport/mqtt.py, tests/test_transport_mqtt.py
The MQTT client now tracks subscribed topics in a set, subscribes missing topics individually, and clears or preserves that set across create, start, connect-failure, and disconnect paths.
Watchdog recovery
custom_components/addhon/client/transport/mqtt.py, tests/test_transport_mqtt.py
_watchdog() now treats health as connected plus full topic coverage, retries partial misses in place, and rebuilds on sustained gaps; the regression tests cover lost updates, reconnect thresholds, and generation checks.

Climate mode mapping

Layer / File(s) Summary
HVAC and fan mode nullability
custom_components/addhon/climate.py, tests/test_ac_write_path.py
hvac_mode and fan_mode now return None for unmapped or non-advertised values, and the tests cover mapped, unmapped, advertised, and OFF-precedence cases.

Log privacy and redaction

Layer / File(s) Summary
Redacted entity-add and cleanup logs
custom_components/addhon/__init__.py, custom_components/addhon/select.py, custom_components/addhon/switch.py, tests/test_program_select.py, tests/test_legacy_cleanup.py
Program-select, pause-switch, and legacy cleanup logs now emit redacted appliance IDs, and the tests assert the raw nickname, object id, and unique id strings are absent.
Static log identity guard rules
tests/test_log_identity_redaction.py, tests/test_coordinator_config_entry.py
The guard now flags inline name/nickname resolution in non-DEBUG logger calls, allows the DEBUG case, includes nickName, and keeps the redact_mac import check flexible.

Sequence Diagram(s)

sequenceDiagram
  participant HomeAssistant
  participant _async_register_services
  participant _handle_refresh
  participant coordinator
  HomeAssistant->>_async_register_services: register SERVICE_REFRESH
  HomeAssistant->>_handle_refresh: call addhon.refresh
  _handle_refresh->>coordinator: async_request_refresh()
  coordinator-->>_handle_refresh: result or exception
Loading
sequenceDiagram
  participant MQTTBroker
  participant NativeMqttClient
  participant HonAppliance
  MQTTBroker->>NativeMqttClient: $aws/events/presence/...
  NativeMqttClient-->>MQTTBroker: ignore for liveness
  MQTTBroker->>NativeMqttClient: appliancestatus payload
  NativeMqttClient->>HonAppliance: mark_realtime_seen(timestamp)
  MQTTBroker->>NativeMqttClient: disconnected payload
  NativeMqttClient->>HonAppliance: mark_realtime_disconnected()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tis24dev/addhOn#31: Earlier MQTT transport lifecycle and watchdog changes in the same module line up with the subscription-set and recovery refactor here.

Poem

I thump through refreshes, quick and bright,
With cloud-time hops and liveness light.
I sniff the logs—no names left bare,
And every mode says “None” with care. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches this release PR and identifies the v5.4.0 version update.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/test_ac_write_path.py (1)

507-540: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a missing-machMode read-path case.

This block covers unmapped and non-advertised values, but not the case where settings.machMode is absent altogether. That is a separate branch here, and it's the one still capable of surfacing a guessed COOL value instead of None.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_ac_write_path.py` around lines 507 - 540, Add a missing-read-path
test for absent machMode in the climate entity logic. In the test class around
_climate and hvac_mode, create a case where settings.onOffStatus is "1" but
settings.machMode is not present at all, then assert that entity.hvac_mode
returns None rather than a guessed HVACMode.COOL. This should cover the separate
branch from the existing unmapped and non-advertised machMode tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@custom_components/addhon/client/transport/mqtt.py`:
- Around line 284-300: The MQTT presence handling in `client/transport/mqtt.py`
still falls through after the `$aws/events/presence/` debug log, so ignored
client-session events can still reach `self._hon.notify()` and the INFO payload
summary. Add an early return in that presence branch inside the MQTT message
handler so AWS session presence topics are fully ignored and do not trigger HA
updates or extra logging.
- Around line 468-474: The `_subscribe_missing()` loop currently only catches
`HonCodedError`, so other subscribe failures from `_subscribe_topic()` can stop
the remaining topics from being processed. Update the exception handling in
`_subscribe_missing()` (and the `_subscribe_topic()` call path if needed) so
non-timeout subscribe errors are also isolated, logged, and skipped, allowing
later topics in the same pass to continue subscribing.

In `@custom_components/addhon/climate.py`:
- Around line 199-211: The climate mode lookup in climate.py is still defaulting
a missing AC_ATTR_MODE/machMode to "1", which causes an absent mode to be
reported as HVACMode.COOL instead of None. Update the mode retrieval in the
climate entity logic around AC_MODE_MAP so a missing raw value is treated as
unknown rather than coerced, preserving the None path already used when the
mapping does not contain a value. Make sure the existing debug/return None
branch is reached for both unmapped and missing machMode values.

---

Nitpick comments:
In `@tests/test_ac_write_path.py`:
- Around line 507-540: Add a missing-read-path test for absent machMode in the
climate entity logic. In the test class around _climate and hvac_mode, create a
case where settings.onOffStatus is "1" but settings.machMode is not present at
all, then assert that entity.hvac_mode returns None rather than a guessed
HVACMode.COOL. This should cover the separate branch from the existing unmapped
and non-advertised machMode tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: bd5dc095-0834-4841-8ccd-697cc9d7ff11

📥 Commits

Reviewing files that changed from the base of the PR and between 51aeee0 and 2e146eb.

📒 Files selected for processing (24)
  • .github/FUNDING.yml
  • README.md
  • custom_components/addhon/__init__.py
  • custom_components/addhon/client/engine/appliance.py
  • custom_components/addhon/client/helpers.py
  • custom_components/addhon/client/transport/mqtt.py
  • custom_components/addhon/climate.py
  • custom_components/addhon/const.py
  • custom_components/addhon/manifest.json
  • custom_components/addhon/select.py
  • custom_components/addhon/services.yaml
  • custom_components/addhon/switch.py
  • custom_components/addhon/translations/en.json
  • custom_components/addhon/translations/it.json
  • tests/test_ac_write_path.py
  • tests/test_client_str_to_float.py
  • tests/test_coordinator_config_entry.py
  • tests/test_engine_appliance_root.py
  • tests/test_hon_client_realtime.py
  • tests/test_legacy_cleanup.py
  • tests/test_log_identity_redaction.py
  • tests/test_program_select.py
  • tests/test_refresh_service.py
  • tests/test_transport_mqtt.py

Comment thread custom_components/addhon/client/transport/mqtt.py
Comment thread custom_components/addhon/client/transport/mqtt.py
Comment thread custom_components/addhon/climate.py Outdated
tis24dev added 2 commits June 26, 2026 23:44
…ng too

_subscribe_missing caught only HonCodedError (the per-topic timeout), so any other
exception from client.subscribe()/the awscrt future (transport or awscrt errors)
escaped the loop and aborted the pass, re-opening the H1 starvation it was meant to
close: every later topic in that pass was skipped. Worse, such an escaped exception
hit the watchdog's outer `except Exception`, which backs off but does NOT increment
resubscribe_failures, so an all-topics-fail-non-timeout dead socket would never
escalate to a token-refreshing rebuild.

Broaden the per-topic handler to isolate EVERY non-cancellation failure (re-raise
CancelledError so stop() is never deadlocked, then `except Exception` -> log + skip).
A failed topic stays missing and is retried next tick; if nothing subscribes the
watchdog's inline blackout escalation now fires correctly. Flagged by the v5.4.0
release review (CodeRabbit, Major).

Adds tests: a non-timeout subscribe error is isolated (the other topics still
subscribe), and a forward-protection guard that the handler keeps the CancelledError
re-raise and does not switch to `except BaseException` (which would swallow
cancellation and deadlock shutdown).
…-mode, monotonic TTL)

Three small fixes from the v5.4.0 release review (CodeRabbit / Greptile):

- mqtt.py: the $aws/events/presence/ branch now returns after the debug log
  instead of falling through to self._hon.notify() and the INFO payload log. Our
  client's own session presence changes no appliance state, so it must not push a
  spurious HA state update nor echo the session payload on every (re)connect.
- climate.py: hvac_mode no longer defaults an absent machMode to "1" (which
  reported COOL). A powered-on unit with no settings.machMode now returns None
  (unknown), closing the gap in the unmapped/non-advertised None contract. (Note:
  fan_mode's "0" default is already safe because "0" is intentionally not in
  AC_FAN_MAP, so a missing windSpeed already yields None.)
- appliance.py: the realtime freshness window (_REALTIME_LIVENESS_TTL) now uses
  time.monotonic() instead of a naive datetime.now(), so it is immune to wall-clock
  jumps (NTP steps, DST transitions). _last_realtime_ts (the cloud timestamp used
  for ordering vs the disconnect event) is unchanged.

Tests: presence events now assert no coordinator notify; a missing-machMode read
path returns None; the freshness-expiry test ages the monotonic mark.
@tis24dev
tis24dev merged commit 3f292d4 into main Jun 26, 2026
9 checks passed
This was referenced Jun 28, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 14, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jul 28, 2026
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.

1 participant