Skip to content

fix: harden setup, mqtt subscribe and CI actions - #49

Merged
tis24dev merged 3 commits into
tis24dev:devfrom
telard-pixel:fix/resilience-ci-hardening
Jul 6, 2026
Merged

fix: harden setup, mqtt subscribe and CI actions#49
tis24dev merged 3 commits into
tis24dev:devfrom
telard-pixel:fix/resilience-ci-hardening

Conversation

@telard-pixel

@telard-pixel telard-pixel commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary by Sourcery

Harden setup, MQTT subscription handling, entity data access, and CI validation actions for the addhon integration.

Bug Fixes:

  • Allow recovering existing config entries that still store the account identifier under the legacy username key instead of email.
  • Guard entity appliance data access against non-dict coordinator payloads to avoid runtime errors.
  • Ensure sensor timestamp generation works with both modern Home Assistant dt.now APIs and older dt.utcnow-only shims.
  • Avoid race conditions when awaiting MQTT subscription completion by fast-pathing already-resolved futures.

CI:

  • Pin hassfest and HACS GitHub Actions to specific commit SHAs in the CI workflow for more reliable and reproducible validation runs.

Summary by CodeRabbit

  • Bug Fixes
    • Improved setup compatibility for older saved accounts, helping existing installations load even if the account email was stored differently.
    • Made entity data handling more resilient so devices are less likely to fail during startup or before the first refresh.
    • Fixed a race condition in MQTT communication that could affect connection setup and subscriptions.
    • Improved timestamp handling for the “last refresh” sensor across different environments.

Greptile Summary

This PR applies targeted hardening fixes across setup, MQTT subscription, entity data access, and sensor timestamps, plus pins CI action SHAs to specific commits for reproducibility.

  • __init__.py: Recovers legacy config entries that stored the account identifier under "username" instead of "email", migrating the key in-place on first successful setup.
  • base_entity.py: Guards _appliance_data against non-dict coordinator payloads that can appear before the first successful refresh.
  • mqtt.py: Fast-paths already-resolved concurrent.futures.Future objects to avoid a race where wait_for times out before the event-loop callback can copy the result.
  • sensor.py / tests/: Replaces the deprecated dt_util.utcnow() with dt_util.now(UTC) to return a timezone-aware datetime, which the TIMESTAMP device class requires; the test stub is updated in lock-step.

Confidence Score: 5/5

All changes are well-scoped defensive fixes with no regressions introduced; safe to merge.

Each change addresses a concrete, narrow failure mode: legacy key migration, coordinator payload guard, MQTT future timing, and deprecated datetime API. The logic is straightforward, the test stub is updated in lock-step, and no new code paths introduce unexpected side effects. Previously flagged observations are minor and do not affect correctness of the happy path.

No files require special attention.

Important Files Changed

Filename Overview
.github/workflows/ci.yml Pins hassfest and HACS actions from floating branch refs to specific commit SHAs — straightforward supply-chain hygiene.
custom_components/addhon/init.py Adds fallback to legacy "username" key and migrates it to "email" in-place; stale "username" key is not removed (previously flagged in thread).
custom_components/addhon/base_entity.py Adds two isinstance guards so _appliance_data safely returns {} when coordinator data is not yet a dict or the appliance entry is not a dict.
custom_components/addhon/client/transport/mqtt.py Fast-paths already-resolved futures to avoid event-loop timing race; non-timeout exception on the fast path skips HonCodedError wrapping (previously flagged in thread).
custom_components/addhon/sensor.py Replaces deprecated dt_util.utcnow() with dt_util.now(UTC) to produce a timezone-aware datetime required by the TIMESTAMP device class.
tests/test_debug_panel.py Updates the dt_util test stub from utcnow to now(tz) in lock-step with the sensor.py change; stub signature correctly handles the UTC argument.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[async_setup_entry] --> B{entry.data has 'email'?}
    B -- Yes --> D[use email directly]
    B -- No --> C{entry.data has 'username'?}
    C -- Yes --> E[email = username value]
    C -- No --> F[email = None → return False]
    E --> G[migrate: add 'email' key to entry.data]
    G --> D
    D --> H[HonClient setup]

    H --> I[NativeMqttClient._subscribe_topic]
    I --> J{future.done?}
    J -- Yes --> K[future.result — raise if error]
    J -- No --> L[asyncio.wait_for wrap_future]
    L -- timeout --> M[raise HonCodedError]
    L -- success --> N[subscribed]
    K --> N

    H --> O[HonBaseEntity._appliance_data]
    O --> P{coordinator.data is dict?}
    P -- No --> Q[return empty dict]
    P -- Yes --> R{entry is dict?}
    R -- No --> Q
    R -- Yes --> S[return entry]

    H --> T[HonLastRefreshSensor._now]
    T --> U[dt_util.now UTC → tz-aware datetime]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[async_setup_entry] --> B{entry.data has 'email'?}
    B -- Yes --> D[use email directly]
    B -- No --> C{entry.data has 'username'?}
    C -- Yes --> E[email = username value]
    C -- No --> F[email = None → return False]
    E --> G[migrate: add 'email' key to entry.data]
    G --> D
    D --> H[HonClient setup]

    H --> I[NativeMqttClient._subscribe_topic]
    I --> J{future.done?}
    J -- Yes --> K[future.result — raise if error]
    J -- No --> L[asyncio.wait_for wrap_future]
    L -- timeout --> M[raise HonCodedError]
    L -- success --> N[subscribed]
    K --> N

    H --> O[HonBaseEntity._appliance_data]
    O --> P{coordinator.data is dict?}
    P -- No --> Q[return empty dict]
    P -- Yes --> R{entry is dict?}
    R -- No --> Q
    R -- Yes --> S[return entry]

    H --> T[HonLastRefreshSensor._now]
    T --> U[dt_util.now UTC → tz-aware datetime]
Loading

Comments Outside Diff (1)

  1. custom_components/addhon/client/transport/mqtt.py, line 457-464 (link)

    P2 Non-timeout exception escapes HonCodedError wrapping on the fast path

    When future.done() is True and the future completed with an error (e.g. an awscrt transport failure), future.result() re-raises that exception directly. Because only asyncio.TimeoutError is caught in this try block, the raw awscrt exception propagates to _subscribe_missing's generic except Exception handler and is logged as a warning — which is the right outcome, but the exception is no longer wrapped in HonCodedError as it would be through the slow path. If callers or logging consumers depend on HonCodedError for error attribution (e.g. metrics or structured error codes), the fast-path bypasses that contract.

Reviews (2): Last reviewed commit: "fix(sensor): simplify _now() to dt_util...." | Re-trigger Greptile

@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 →

@sourcery-ai

sourcery-ai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR hardens integration setup, MQTT subscription handling, entity data access, sensor time generation, and CI workflows to be more tolerant of legacy/corrupt data and external changes while pinning GitHub Actions to specific commits for reproducibility and security.

Sequence diagram for hardened async_setup_entry email/username migration

sequenceDiagram
    participant HomeAssistant
    participant ConfigEntry
    participant async_setup_entry
    participant HonClient

    HomeAssistant->>async_setup_entry: async_setup_entry(hass, entry)
    async_setup_entry->>ConfigEntry: entry.data.get(email)
    async_setup_entry->>ConfigEntry: entry.data.get(username)
    alt email_missing_and_username_present
        async_setup_entry->>HomeAssistant: hass.config_entries.async_update_entry(entry, data)
    end
    async_setup_entry->>HonClient: HonClient(email, password, refresh_token)
    HonClient-->>async_setup_entry: client_created
    async_setup_entry-->>HomeAssistant: True/False
Loading

Sequence diagram for MQTT _subscribe_topic fast-path handling

sequenceDiagram
    participant MQTTClient
    participant MQTTSocket
    participant Future

    MQTTClient->>MQTTSocket: send_packet(mqtt5.SubscribePacket)
    MQTTSocket-->>Future: set_result()
    MQTTClient->>MQTTClient: _subscribe_topic(topic)
    alt future_done
        MQTTClient->>Future: future.result()
    else future_not_done
        MQTTClient->>Future: asyncio.wrap_future(future)
        MQTTClient->>Future: asyncio.wait_for(..., _SUBSCRIBE_TIMEOUT)
    end
Loading

File-Level Changes

Change Details Files
Make config entry setup resilient to legacy entries using the old "username" key and migrate them to the canonical "email" field.
  • Derive the email value from the primary "email" key, falling back to the legacy "username" key when needed.
  • Detect entries lacking "email" but having "username" and update the entry data to include "email" so future runs use the normalized field.
custom_components/addhon/__init__.py
Avoid race conditions and spurious timeouts when subscribing to MQTT topics by short‑circuiting already‑resolved futures.
  • Check whether the subscription future is already done and synchronously consume its result without wrapping it in wait_for.
  • Only use asyncio.wait_for with the configured timeout when the future is still pending, keeping existing timeout handling intact.
custom_components/addhon/client/transport/mqtt.py
Defensively access coordinator appliance data to handle non-dict coordinator payloads and malformed entries.
  • Validate that coordinator.data is a dict before indexing by appliance id, returning an empty dict otherwise.
  • Ensure the per-appliance entry is itself a dict; if not, return an empty dict to protect entity logic from unexpected types.
custom_components/addhon/base_entity.py
Make the sensor’s notion of "now" compatible with newer Home Assistant dt utilities while remaining robust to older shims and test stubs.
  • Import the standard UTC timezone object for use with dt_util.now when available.
  • Prefer dt_util.now(UTC) when the function exists and is callable, falling back to dt_util.utcnow() when running against older or stubbed environments.
custom_components/addhon/sensor.py
Harden CI workflows by pinning Home Assistant and HACS GitHub Actions to specific commit SHAs instead of floating branches.
  • Replace the hassfest action reference from the master branch to a fixed commit hash.
  • Replace the HACS validation action reference from the main branch to a fixed commit hash for reproducible CI behavior.
.github/workflows/ci.yml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2a1b3c5-dc94-41b4-b518-ebc0c8813bed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR pins two GitHub Actions in the CI workflow to fixed commit SHAs, and applies defensive fixes to the addhOn integration: config entry email/username migration fallback, coordinator data type guarding, an MQTT subscribe future fast-path, and a sensor timestamp fallback.

Changes

Robustness and CI Fixes

Layer / File(s) Summary
Legacy config entry email migration
custom_components/addhon/__init__.py
async_setup_entry falls back to entry.data["username"] when email is missing, and migrates entry.data to add email for legacy entries.
Defensive coordinator data access
custom_components/addhon/base_entity.py
HonBaseEntity._appliance_data now checks that coordinator.data and the per-appliance entry are dicts before use, returning {} otherwise.
MQTT subscribe fast-path
custom_components/addhon/client/transport/mqtt.py
_subscribe_topic checks future.done() and reads future.result() immediately instead of always awaiting wait_for/wrap_future.
Sensor timestamp fallback
custom_components/addhon/sensor.py
HonLastRefreshSensor._now prefers homeassistant.util.dt.now(UTC) when available, falling back to dt_util.utcnow().
CI action SHA pinning
.github/workflows/ci.yml
hassfest and hacs/action step references are pinned to fixed commit SHAs instead of @master/@main.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

  • tis24dev/addhOn#36: Both PRs modify NativeMqttClient's subscription flow in mqtt.py, directly related at the subscribe implementation level.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise and accurately summarizes the main hardening changes to setup, MQTT subscribe handling, and CI actions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="custom_components/addhon/sensor.py" line_range="1115-1122" />
<code_context>
     def _now():
         # Lazy dt import: keeps the test stubs (which import this module but never
         # drive a coordinator update) free of a homeassistant.util.dt stub.
+        from datetime import UTC
+
         from homeassistant.util import dt as dt_util

+        now = getattr(dt_util, "now", None)
+        if callable(now):
+            return now(UTC)
+        # Test stubs and older HA shims may only provide utcnow().
         return dt_util.utcnow()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Handle dt_util.now implementations that don’t accept a timezone argument

There’s a corner case where `dt_util.now` exists but doesn’t accept a `tz` argument: `callable(now)` is true, but `now(UTC)` would raise `TypeError`, never reaching the utc fallback. To keep this robust across dt utility variants, wrap `now(UTC)` in `try`/`except TypeError` and fall back to `dt_util.utcnow()` on error.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread custom_components/addhon/sensor.py Outdated
Comment on lines +1115 to +1122
from datetime import UTC

from homeassistant.util import dt as dt_util

now = getattr(dt_util, "now", None)
if callable(now):
return now(UTC)
# Test stubs and older HA shims may only provide utcnow().

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Handle dt_util.now implementations that don’t accept a timezone argument

There’s a corner case where dt_util.now exists but doesn’t accept a tz argument: callable(now) is true, but now(UTC) would raise TypeError, never reaching the utc fallback. To keep this robust across dt utility variants, wrap now(UTC) in try/except TypeError and fall back to dt_util.utcnow() on error.

Comment thread custom_components/addhon/sensor.py Outdated
Comment on lines 1119 to 1123
now = getattr(dt_util, "now", None)
if callable(now):
return now(UTC)
# Test stubs and older HA shims may only provide utcnow().
return dt_util.utcnow()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The guard callable(now) only verifies that the attribute exists and is callable — it does not verify the function's signature. A test stub that defines now as a zero-argument function (e.g. def now(): ...) would pass the callable check and then raise TypeError when called as now(UTC), bypassing the utcnow() fallback entirely. Wrapping the call in a try/except TypeError keeps the fallback reachable in all stub configurations.

Suggested change
now = getattr(dt_util, "now", None)
if callable(now):
return now(UTC)
# Test stubs and older HA shims may only provide utcnow().
return dt_util.utcnow()
now = getattr(dt_util, "now", None)
if callable(now):
try:
return now(UTC)
except TypeError:
pass
# Test stubs and older HA shims may only provide utcnow().
return dt_util.utcnow()

Comment thread custom_components/addhon/__init__.py Outdated
Comment on lines +423 to +426
if "email" not in entry.data and entry.data.get("username"):
hass.config_entries.async_update_entry(
entry, data={**entry.data, "email": email}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The migration writes "email" into the config entry but leaves the old "username" key in place. That stale key is harmless today, but any code that iterates entry.data keys or inspects them for diagnostics will see an unexpected key. Dropping "username" in the same update keeps the entry data clean.

Suggested change
if "email" not in entry.data and entry.data.get("username"):
hass.config_entries.async_update_entry(
entry, data={**entry.data, "email": email}
)
if "email" not in entry.data and entry.data.get("username"):
migrated = {k: v for k, v in entry.data.items() if k != "username"}
migrated["email"] = email
hass.config_entries.async_update_entry(entry, data=migrated)

@tis24dev
tis24dev changed the base branch from main to dev July 6, 2026 14:20
Drop the getattr/callable fallback dance from the PR's _now(): production HA
always exposes dt_util.now(), so a plain dt_util.now(UTC) is enough (still a
tz-aware UTC datetime, as the TIMESTAMP device class requires). The only stub
that drives HonLastRefreshSensor (test_debug_panel lifecycle tests) now provides
dt.now instead of dt.utcnow, keeping the compatibility shim in the test rather
than in production.
tis24dev added a commit that referenced this pull request Jul 6, 2026
The username->email migration merged {**entry.data, "email": email}, which
left the stale "username" key in the config entry data. Rebuild the dict
without "username" in the same update so migrated entries carry only "email"
(no unexpected key for diagnostics/iteration). Addresses Greptile P2 on PR #49.
# Conflicts:
#	custom_components/addhon/__init__.py
@tis24dev
tis24dev merged commit 151af6a into tis24dev:dev Jul 6, 2026
5 checks passed
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.

2 participants