Skip to content

Release v5.8.0 - #48

Merged
tis24dev merged 4 commits into
mainfrom
dev
Jul 6, 2026
Merged

Release v5.8.0#48
tis24dev merged 4 commits into
mainfrom
dev

Conversation

@tis24dev

@tis24dev tis24dev commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Automated release PR for v5.8.0.

Summary by Sourcery

Improve the addhon integration’s reliability, AC program handling, command/program execution, auth/transport behavior, diagnostics privacy, and debug options, and bump the integration to v5.8.0.

New Features:

  • Add explicit support for program-based AC write models that drive power and mode via startProgram/stopProgram rather than settings.
  • Disambiguate duplicate washer program labels in the program select entity so each program code remains uniquely selectable.

Bug Fixes:

  • Fix AC climate control for program-based models by routing on/off and hvac_mode to startProgram/stopProgram while keeping temperature and fan on settings.
  • Ensure settings-based AC models continue to use the settings write path even when program commands are present (regression guard).
  • Single-flight concurrent full re-auth attempts so request bursts do not trigger multiple logins or OTP prompts, including in failure cases.
  • Treat HTML 403 responses as transient edge challenges instead of auth failures so they don’t trigger refresh/re-auth ladders.
  • Prevent favourites and their rule copies from mutating base program commands or sharing trigger tables, keeping base programs pristine.
  • Guard malformed fixed-value rules so they are skipped at construction and runtime instead of raising and breaking command loading.
  • Rebind HonParameterProgram back-references when copying commands so writes on copies cannot affect the base command.
  • Roll back command swaps and parameter mutations on failed program start button sends so local state matches what the cloud accepted.
  • Roll back pause parameter changes on failed switch sends so local pause state is not desynchronized from the cloud.
  • Avoid dumping huge enumerated grids for range parameters in diagnostics and only emit min/max/step.
  • Mask MAC addresses and similar identities even when they appear in non-redacted value fields or wrapped objects in diagnostics.
  • Normalize trigger comparison in parameter base so default values that match string triggers still fire initial rules.
  • Make entity availability robust when attributes are missing or string-encoded and ensure coordinator data type checks before iteration.
  • Handle non-numeric or empty applianceModelId values defensively to avoid ValueError.
  • Abort config reauth flows cleanly when the target entry has been removed instead of failing later.
  • Guard coordinator.data accesses in select, binary_sensor, and sensor setup when data is not a dict.
  • Fix auth cookie clearing to actually drop cookies for the auth host instead of being a no-op, avoiding stale SSO cookie reuse.
  • Ensure the debug options update listener only re-applies log levels when toggles change, preserving runtime-set debug levels.

Enhancements:

  • Add capability gating and detailed error reporting for unsupported AC programs and swing positions, aligning with translation keys.
  • Introduce helper utilities for program capability discovery and async program sending in the AC climate component.
  • Enhance diagnostics schema generation and identity redaction to better mirror runtime behavior and logging privacy guarantees.
  • Refine rule attachment and rebinding in the engine so copied commands carry isolated triggers and configuration rules.
  • Improve base entity availability handling to share the same normalization logic as other attributes.
  • Make options flow debug handling track and store current toggle state for later change detection.

Build:

  • Update the integration manifest version from 5.7.2 to 5.8.0.

Tests:

  • Add comprehensive tests for program-based AC write paths, including program mapping, OFF handling, error cases, and regression behavior for settings-based models.
  • Add transport connection tests covering single-flighted concurrent re-auth, HTML vs JSON 403 behavior, and retry ladders.
  • Add engine cluster tests to validate favourites isolation, rule copying behavior, malformed rule tolerance, and program parameter rebinding.
  • Extend program select tests to cover duplicate-label disambiguation and rollback behavior on failed program command sends.
  • Add options flow tests ensuring debug level updates only occur when toggles actually change.
  • Expand diagnostics tests to cover range schema generation without enums and MAC masking in values and wrapped objects.

Summary by CodeRabbit

  • New Features

    • Added support for more appliance control modes, including program-based AC power/mode handling and improved program selection.
    • Diagnostics now better hide sensitive device details in shared reports.
  • Bug Fixes

    • Improved reliability when changing settings or starting programs, including safer rollback if a command fails.
    • Fixed re-authentication and login handling so recovery is more stable.
    • Prevented crashes from unexpected device data and improved availability/status accuracy.
  • Chores

    • Updated the integration version.

Greptile Summary

This release bundles a large set of targeted bug fixes and one new feature: explicit support for program-based AC models (e.g. AD71S2SM3FA(H)) that drive power and mode via startProgram/stopProgram rather than the settings command. All other fixes address concrete defects in auth, engine state isolation, rollback behaviour, diagnostics privacy, and platform setup guards.

  • Program-based AC write path (climate.py, const.py, program_options.py): adds _is_program_based() capability gate; routes ON/mode to startProgram and OFF to stopProgram, while temperature and fan remain on settings. Regression guard ensures settings-based models keep their existing path even when startProgram/stopProgram are present.
  • Single-flight concurrent re-auth (connection.py): introduces _reauth_after_rejection with generation-keyed error caching so a burst of failing requests collapses to one create()+authenticate() instead of triggering N logins/OTP prompts; HTML 403s are redirected to the DECODE_ERROR transient path.
  • Engine state isolation and rollback (commands.py, rules.py, param_rollback.py, hon_commands.py, button.py, switch.py): HonCommand.__copy__ gives favourites isolated parameter dicts and rebound trigger tables; HonRuleSet.rebound re-attaches triggers against the copy; shared snapshot_params/restore_params helpers centralise rollback for all send paths.

Confidence Score: 5/5

Safe to merge — all fixes address well-defined defects, each has a targeted test, and no regressions were found across the settings-based and program-based AC paths.

The changes are surgical and backed by comprehensive tests covering the new program-based AC write path, concurrent re-auth single-flighting (including the failure case), engine favourite isolation, rollback behaviour, and diagnostics privacy. The auth cookie-clear fix and the select disambiguation are straightforward corrections with no observable side-effects on the existing flows. No unguarded mutation paths, missing rollback cases, or logic inversions were identified during review.

No files require special attention. The most complex change is connection.py's _reauth_after_rejection, which is well-commented and covered by two dedicated concurrency tests.

Important Files Changed

Filename Overview
custom_components/addhon/climate.py Adds program-based AC write path with capability gating; settings-based regression guard verified; swing-position fallback now raises instead of sending wrong oscillation code.
custom_components/addhon/client/transport/connection.py Single-flight concurrent re-auth via generation-keyed error cache; HTML 403 short-circuited to DECODE_ERROR; logic correct for success, failure, and CancelledError cases.
custom_components/addhon/client/engine/commands.py HonCommand.copy now isolates _parameters, resets trigger tables, rebinds HonParameterProgram back-references, and rebound rule sets — correctly prevents favourite mutation from corrupting base commands.
custom_components/addhon/client/engine/rules.py patch() refactored into _attach_triggers/_apply_config_rules; new rebound() creates an isolated HonRuleSet bound to a copied command; malformed fixedValue rules caught and logged instead of aborting appliance setup.
custom_components/addhon/param_rollback.py New shared snapshot_params/restore_params helpers centralise dict-level parameter rollback; correctly bypasses setters so rules are not re-fired on restore.
custom_components/addhon/client/transport/auth.py clear() now correctly derives the auth host via urlsplit().netloc (fixing the no-op clear_domain('') bug); introduce path appends trailing '&' to capture the last fragment field and gates on t.complete before committing tokens.
custom_components/addhon/select.py Duplicate-label disambiguation via _program_display ensures every program code is uniquely selectable; all current_option lookups updated to use disambiguated labels.
custom_components/addhon/button.py Adds rollback dict to capture pre-swap command pointer and two-phase parameter snapshots; correctly restores in reverse order on send failure.
custom_components/addhon/diagnostics.py _param_schema now emits min/max/step for range params instead of enumerating the full grid; _jsonable applies _MAC_RE redaction to all string values including wrapped objects.
custom_components/addhon/init.py _async_options_updated now stores current debug toggle state and skips re-apply when only non-debug data (e.g. token rotation) changed, preserving runtime-set log levels.
custom_components/addhon/client/engine/parameter/base.py add_trigger now normalises both sides with str().lower() to match check_trigger semantics; reset_triggers() creates a fresh dict (not .clear()) to avoid emptying the shared trigger table on copy.

Comments Outside Diff (1)

  1. custom_components/addhon/select.py, line 477-482 (link)

    P2 After the disambiguation refactor, _program_map (code → original label) still appears in the "pending code not in map" debug message. Since users now see disambiguated labels from _program_display, logging _program_display would make it easier to cross-reference the debug output with what the UI actually shows.

    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: "refactor: address CodeRabbit nitpicks on..." | Re-trigger Greptile

telard-pixel and others added 3 commits July 6, 2026 11:02
…Program)

AC models whose `settings` command has no onOffStatus (e.g. AD71S2SM3FA(H))
drive power/mode via startProgram/stopProgram, not via onOffStatus/machMode in
`settings`. The climate entity only ever wrote into `settings`, so on/off and
mode commands raised "Parameter(s) not found" before the request reached the
cloud and every command looked ignored; temp/fan were sent but had no visible
effect while the unit was off.

Route the write path by capability: when `settings` lacks onOffStatus and a
startProgram command exists, send OFF via stopProgram, a concrete mode via
startProgram with the mapped iot_<mode> program (AC_PROGRAM_MAP; FAN_ONLY maps
to iot_fan, not iot_fan_only), and turn_on via iot_simple_start. The program is
capability-gated against the device's live startProgram enum and raises
program_not_supported when absent (no silent fallback to the settings path).
Temperature and fan stay on the settings path (tempSel/windSpeed exist on
`settings` in both models). Settings-based models (AS35PBPHRA-PRE) are
unchanged; the read path is untouched.

Reuses program_options.async_send_program (category-swap aware) for startProgram
and a generic stopProgram send. Adds program-based write-path tests plus an
AS35-style regression guard that keeps settings-based on/off/mode even when
startProgram/stopProgram also exist. Full suite green (1023 passed, 1 skipped).
@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

Release v5.8.0 extends addhon’s program/command engine, transport auth, diagnostics, and HA entities with robustness fixes, program-based AC write-path support, safer favourite/program handling, better rollback on failed sends, and more resilient options/debug handling, plus tests for all new behaviours.

Sequence diagram for program-based vs settings-based AC write path

sequenceDiagram
    actor User
    participant HomeAssistant as HomeAssistant
    participant ClimateEntity as AddhonClimate
    participant HonClient as HonClient
    participant HonAPI as HonAPI

    User->>HomeAssistant: change HVACMode / turn_on
    HomeAssistant->>ClimateEntity: async_set_hvac_mode(hvac_mode)
    ClimateEntity->>ClimateEntity: _is_program_based()
    alt program_based & hvac_mode == OFF
        ClimateEntity->>HonClient: async_send_command(HonAPI, stopProgram)
    else program_based & hvac_mode != OFF
        ClimateEntity->>ClimateEntity: _program_for_mode(hvac_mode)
        ClimateEntity->>HonClient: async_send_program(HonAPI, program_code)
    else settings_based & hvac_mode == OFF
        ClimateEntity->>HonClient: _send_command_in_executor(onOffStatus="0")
    else settings_based & hvac_mode != OFF
        ClimateEntity->>HonClient: _send_command_in_executor(onOffStatus="1", machMode)
    end

    User->>HomeAssistant: turn_on
    HomeAssistant->>ClimateEntity: async_turn_on()
    ClimateEntity->>ClimateEntity: _is_program_based()
    alt program_based
        ClimateEntity->>ClimateEntity: _assert_program_available(AC_PROGRAM_SIMPLE_START)
        ClimateEntity->>HonClient: async_send_program(HonAPI, AC_PROGRAM_SIMPLE_START)
    else settings_based
        ClimateEntity->>HonClient: _send_command_in_executor(onOffStatus="1")
    end
Loading

File-Level Changes

Change Details Files
Add program-based AC write path for Haier climate entities and regression guard for settings-based models.
  • Introduce AC_ON_OFF_PARAM, AC_PROGRAM_MAP, AC_PROGRAM_SIMPLE_START, and PROGRAM_PARAM_NAMES constants for program-based AC handling.
  • Implement HaierClimateEntity._is_program_based, _startprogram_programs, _program_for_mode, and _assert_program_available to route ON/OFF/mode to startProgram/stopProgram when settings lacks onOffStatus.
  • Update async_set_hvac_mode and async_turn_on to call async_send_program/async_send_command instead of direct settings writes when program-based.
  • Add swing_mode guard that raises swing_position_not_allowed when fixed_vertical_value falls back to swing-on code.
custom_components/addhon/const.py
custom_components/addhon/climate.py
tests/test_ac_write_path.py
Single-flight re-auth logic in transport connection including failure caching and special handling for HTML 403 edge challenges.
  • Add _reauth_error/_reauth_error_gen tracking and implement _reauth_after_rejection under the same lock/generation as refresh.
  • Update _intercept loop-1 branch to use _reauth_after_rejection and ensure refresh_gen advancement and reuse of cached errors.
  • Introduce _is_html_challenge and treat HTML 403 as DECODE_ERROR without entering refresh/reauth ladder.
  • Add tests covering concurrent reauth success/failure, HTML 403 transient handling, and JSON 403 reauth ladder behaviour.
custom_components/addhon/client/transport/connection.py
tests/test_transport_connection.py
Make command/favourite/program handling safe by deep-copying parameters, rebinding rule sets/program backrefs, and skipping malformed rules at runtime and construction.
  • Implement HonCommand.copy to copy parameter objects, reset triggers, and rebind HonParameterProgram._command/_programs, and rulesets via HonRuleSet.rebound.
  • Add BaseParameter.reset_triggers and change add_trigger immediate-fire to normalize value comparison via str().
  • Extend HonRuleSet with _attach_triggers and rebound to create per-command rule sets; wrap rule application in try/except to swallow ValueError/TypeError and log skips.
  • Add multiple regression tests ensuring favourites don’t corrupt base programs, rule copies don’t affect base, malformed fixedValue rules are tolerated, and program param backrefs are rebound.
custom_components/addhon/client/engine/commands.py
custom_components/addhon/client/engine/parameter/base.py
custom_components/addhon/client/engine/rules.py
tests/test_engine_cluster.py
Improve entity/UI behaviour: robust coordinator.data iteration, AC program select disambiguation, command/button rollback on send failure, and pause switch rollback.
  • Guard coordinator.data accesses in select, binary_sensor, and sensor setup by treating non-dict data as empty maps.
  • Update HonProgramSelect to disambiguate duplicate program labels via code suffix and track display vs code mapping; adjust current_option to use display labels.
  • Enhance HonProgramCommandButton.async_press with rollback snapshots of commands and parameters; on send failure, restore the original command and param state and keep pending programs.
  • Modify switch pause sending to snapshot pause parameter state and restore on send failure via dict mutation rollback.
custom_components/addhon/select.py
custom_components/addhon/button.py
custom_components/addhon/switch.py
tests/test_program_select.py
Strengthen diagnostics and base entity availability semantics; mask MAC addresses in values and avoid huge range enums in schema.
  • Change diagnostics._param_schema to emit min/max/step for range params only, never enumerated .values grids in enum.
  • Use MAC regex from debug_utils in diagnostics to redact MACs in any string value or wrapper.value before JSON serialization; extend sensitive key list with transaction_id.
  • Add tests asserting range schema omits enum, MAC masking in direct values and wrapped objects.
  • Update BaseEntity.available to short-circuit on _present, use _get_attr for available, and normalize string booleans before bool().
custom_components/addhon/diagnostics.py
custom_components/addhon/base_entity.py
tests/test_diagnostics.py
Harden transport auth: correctly parse full OAuth token fragment and clear auth cookies for the real host; add config_flow reauth safety.
  • Modify _introduce to append '&' before parse_token_fragment, require tokens_complete, phase-log incomplete fragments, and raise NativeAuthError instead of proceeding with partial tokens.
  • Update HonAuth.clear to derive auth host via urlsplit(AUTH_API).netloc and clear that domain’s cookies instead of a no-op empty host.
  • Ensure config_flow.async_step_reauth_confirm aborts with reauth_entry_not_found when the reauth entry disappears mid-flow instead of raising AttributeError.
custom_components/addhon/client/transport/auth.py
custom_components/addhon/config_flow.py
Make options flow debug-level updates conditional on actual toggle changes and persist baseline toggle state in hass.data.
  • Introduce _DEBUG_OPTS_KEY and _debug_opts helpers storing integration/mqtt debug toggles per entry.
  • Change _async_options_updated to compare current toggles with stored _DEBUG_OPTS_KEY; if unchanged, skip re-applying log levels to preserve runtime debug adjustments.
  • Store initial _DEBUG_OPTS_KEY in hass.data during setup via _on_realtime_push.
  • Add options flow tests ensuring listener is a no-op when toggles unchanged and reapplies when toggles changed.
custom_components/addhon/__init__.py
tests/test_options_flow.py
Misc robustness improvements: defensive appliance model_id parsing and coordinator.data guards in entity setup; bump manifest version.
  • Update NaAppliance.model_id to safely parse applianceModelId with try/except and fall back to 0 on invalid values.
  • Guard coordinator.data usage in binary_sensor and sensor async_setup_entry by treating non-dicts as empty.
  • Bump manifest.json version from 5.7.2 to 5.8.0.
custom_components/addhon/client/engine/appliance.py
custom_components/addhon/binary_sensor.py
custom_components/addhon/sensor.py
custom_components/addhon/manifest.json

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

Warning

Review limit reached

@tis24dev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 07de5e13-3393-4078-8813-c7ac0c186981

📥 Commits

Reviewing files that changed from the base of the PR and between 97e5f1c and 8155b73.

📒 Files selected for processing (11)
  • custom_components/addhon/base_entity.py
  • custom_components/addhon/binary_sensor.py
  • custom_components/addhon/button.py
  • custom_components/addhon/client/transport/connection.py
  • custom_components/addhon/diagnostics.py
  • custom_components/addhon/hon_commands.py
  • custom_components/addhon/param_rollback.py
  • custom_components/addhon/program_options.py
  • custom_components/addhon/select.py
  • custom_components/addhon/sensor.py
  • custom_components/addhon/switch.py
📝 Walkthrough

Walkthrough

This PR hardens the hOn integration across several areas: debug-toggle baseline caching, availability/coordinator-data normalization, command/parameter copy isolation with trigger-safe rule application, native auth/connection re-auth single-flight and cookie-clear fixes, AC program-based write routing, config-flow reauth guard, diagnostics redaction/schema fixes, program-select label disambiguation, and rollback-on-failure for button/switch commands, plus corresponding tests and translations.

Changes

Core reliability fixes and AC program-based control

Layer / File(s) Summary
Debug options baseline tracking
custom_components/addhon/__init__.py, tests/test_options_flow.py
Caches a debug-toggle baseline in hass.data and skips re-applying log levels on options updates unless toggles actually changed.
Availability normalization and coordinator.data guards
custom_components/addhon/base_entity.py, binary_sensor.py, select.py, sensor.py
Normalizes string falsey available values and guards entity setup against non-dict coordinator.data.
Command/parameter copy isolation and rule application safety
custom_components/addhon/client/engine/commands.py, parameter/base.py, rules.py, appliance.py, tests/test_engine_cluster.py
Adds HonCommand.__copy__ isolating parameters/triggers/rules, reset_triggers(), case-insensitive trigger comparison, try/except-guarded rule application, and defensive model_id parsing.
Auth token parsing and connection re-auth single-flight
custom_components/addhon/client/transport/auth.py, connection.py, tests/test_transport_connection.py
Fixes trailing-fragment token parsing and cookie-clear host derivation, and introduces generation-scoped single-flight re-auth with HTML-403 challenge detection.
AC program-based write path
custom_components/addhon/climate.py, const.py, translations, tests/test_ac_write_path.py
Adds constants and logic to route power/mode via startProgram/stopProgram for program-based AC models, with swing-mode safeguards and regression coverage for settings-based devices.
Config flow reauth entry guard
custom_components/addhon/config_flow.py, translations
Aborts reauth confirmation cleanly with reauth_entry_not_found when the target entry no longer exists.
Diagnostics redaction and schema fixes
custom_components/addhon/diagnostics.py, tests/test_diagnostics.py
Extends MAC redaction to plain/wrapped strings, renames a redaction key, and prioritizes range output over enum in parameter schema generation.
Program select label disambiguation
custom_components/addhon/select.py, tests/test_program_select.py
Disambiguates duplicate program display labels and updates option resolution to use the collision-aware mapping.
Button/switch send-failure rollback
custom_components/addhon/button.py, switch.py, tests/test_program_select.py
Snapshots and restores command/parameter state when send() fails during program start or pause/resume actions.
Manifest version bump
custom_components/addhon/manifest.json
Bumps integration version from 5.7.2 to 5.8.0.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RequestA
  participant RequestB
  participant HonConnection
  participant Auth
  RequestA->>HonConnection: 401/403 at loop 1
  HonConnection->>HonConnection: _reauth_after_rejection(gen)
  HonConnection->>Auth: create()/authenticate()
  RequestB->>HonConnection: 401/403 same gen
  HonConnection-->>RequestB: reuse cached result/error
Loading
sequenceDiagram
  participant HonProgramCommandButton
  participant Appliance
  participant Command
  HonProgramCommandButton->>HonProgramCommandButton: snapshot params pre-swap
  HonProgramCommandButton->>Appliance: swap active command
  HonProgramCommandButton->>HonProgramCommandButton: snapshot params post-swap
  HonProgramCommandButton->>Command: send()
  Command-->>HonProgramCommandButton: raises exception
  HonProgramCommandButton->>Appliance: restore original command
  HonProgramCommandButton->>HonProgramCommandButton: restore parameter snapshots
Loading

Possibly related PRs

  • tis24dev/addhOn#31: Overlaps with HonConnection's 401/403 re-auth/reauth retry ladder and generation-scoped single-flight behavior in connection.py.
  • tis24dev/addhOn#38: Both modify the same button.py startProgram button-press flow and its interaction with pending program-option state.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the release-focused changeset and clearly identifies the version bump.
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.
✨ Finishing Touches
🧪 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.

@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, and left some high level feedback:

  • The per-module rollback logic that snapshots and restores parameter __dict__ (button, switch) is quite ad hoc and duplicated; consider centralizing this into a reusable helper on the parameter/command to avoid subtle state mismatches and make future rollback paths easier to maintain.
  • In HonCommand.__copy__, you rely on copy(param) plus manual rebinding for HonParameterProgram; if other parameter subclasses hold back-references or mutable internal structures, they may still leak changes from favourites into the base command, so it may be safer to expose an explicit clone() on parameters instead of a generic copy().
  • The _is_program_based gate in climate.HaierClimateEntity assumes that the absence of onOffStatus and presence of startProgram uniquely identifies program-based models; if hOn adds hybrid or variant models, this heuristic may misroute writes, so it could be worth making the capability detection pluggable or keyed off a more explicit model flag.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The per-module rollback logic that snapshots and restores parameter `__dict__` (button, switch) is quite ad hoc and duplicated; consider centralizing this into a reusable helper on the parameter/command to avoid subtle state mismatches and make future rollback paths easier to maintain.
- In `HonCommand.__copy__`, you rely on `copy(param)` plus manual rebinding for `HonParameterProgram`; if other parameter subclasses hold back-references or mutable internal structures, they may still leak changes from favourites into the base command, so it may be safer to expose an explicit `clone()` on parameters instead of a generic `copy()`.
- The `_is_program_based` gate in `climate.HaierClimateEntity` assumes that the absence of `onOffStatus` and presence of `startProgram` uniquely identifies program-based models; if hOn adds hybrid or variant models, this heuristic may misroute writes, so it could be worth making the capability detection pluggable or keyed off a more explicit model flag.

## Individual Comments

### Comment 1
<location path="tests/test_ac_write_path.py" line_range="653-568" />
<code_context>
+    async def test_program_send_failure_raises_command_error(self) -> None:
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen the failure-path test by asserting that settings/stop commands are not touched when startProgram send fails

In `test_program_send_failure_raises_command_error`, we only assert the translation key, `start.send_calls == 1`, and `coordinator.refreshes == 0`. This failure path should also ensure that no `settings` or `stopProgram` commands are sent. Please add assertions like `self.assertEqual(0, settings.send_calls)` and `self.assertEqual(0, stop.send_calls)` so the test more clearly verifies that the rollback path prevents partial writes to these commands.

Suggested implementation:

```python
        self.assertEqual(1, start.send_calls)
        self.assertEqual(0, settings.send_calls)
        self.assertEqual(0, stop.send_calls)
        self.assertEqual(0, coordinator.refreshes)

```

This change assumes that `settings`, `start`, `stop`, and `coordinator` are all defined within `test_program_send_failure_raises_command_error` as shown in your snippet, and that the existing assertions for `start.send_calls` and `coordinator.refreshes` are exactly as in the SEARCH block. If the assertions differ slightly (e.g., using `assertEqual` with reversed arguments or different spacing), adjust the SEARCH text to match the exact existing lines so the replacement applies correctly.
</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.

entity, settings, start, stop, coord = _program_climate()
await entity.async_turn_on()
# turn_on = resume last mode -> iot_simple_start on startProgram, NOT settings.
self.assertEqual(1, start.send_calls)

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 (testing): Strengthen the failure-path test by asserting that settings/stop commands are not touched when startProgram send fails

In test_program_send_failure_raises_command_error, we only assert the translation key, start.send_calls == 1, and coordinator.refreshes == 0. This failure path should also ensure that no settings or stopProgram commands are sent. Please add assertions like self.assertEqual(0, settings.send_calls) and self.assertEqual(0, stop.send_calls) so the test more clearly verifies that the rollback path prevents partial writes to these commands.

Suggested implementation:

        self.assertEqual(1, start.send_calls)
        self.assertEqual(0, settings.send_calls)
        self.assertEqual(0, stop.send_calls)
        self.assertEqual(0, coordinator.refreshes)

This change assumes that settings, start, stop, and coordinator are all defined within test_program_send_failure_raises_command_error as shown in your snippet, and that the existing assertions for start.send_calls and coordinator.refreshes are exactly as in the SEARCH block. If the assertions differ slightly (e.g., using assertEqual with reversed arguments or different spacing), adjust the SEARCH text to match the exact existing lines so the replacement applies correctly.

@tis24dev

tis24dev commented Jul 6, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Repository owner deleted a comment from coderabbitai Bot Jul 6, 2026

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

🧹 Nitpick comments (6)
custom_components/addhon/sensor.py (1)

837-838: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated coordinator.data dict-guard pattern across platforms.

The same data_map = coordinator.data if isinstance(coordinator.data, dict) else {} guard is duplicated identically in binary_sensor.py, select.py, and sensor.py. Consider extracting a small shared helper (e.g. in base_entity.py or a utils module) to avoid drift if the guard logic ever needs to change.

🤖 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 `@custom_components/addhon/sensor.py` around lines 837 - 838, The repeated
coordinator.data dict-guard is duplicated across the platform entity setup
loops, so extract that logic into a shared helper in base_entity.py or a small
utils module and have sensor.py, binary_sensor.py, and select.py call it instead
of inlining the isinstance check. Keep the helper focused on returning a safe
dict from coordinator.data, and update the setup code that iterates
appliance_id/data to use the shared function so the behavior stays consistent
across platforms.
custom_components/addhon/select.py (1)

325-341: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Label disambiguation and current_option routing look correct.

Collision-suffixing on unique codes keeps _program_reverse injective, and both current_option branches (pending at Line 468, token-in-map at Lines 514-516) now consistently resolve through _program_display, matching the new _attr_options.

One narrow edge case: if a device attribute reports a bare colliding label via programName (Line 517's token in self._program_reverse check), it won't match since reverse-map keys are now suffixed for collisions — it falls through to the next candidate key. This is self-recovering (other keys like prCode/program resolve via the token-in-map branch), so not blocking.

Also applies to: 462-531

🤖 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 `@custom_components/addhon/select.py` around lines 325 - 341, Handle the edge
case where current_option receives a bare colliding program label from
programName, since _program_reverse now only stores suffixed display labels for
collisions. Update HonProgramOptionSelect’s lookup logic in current_option to
normalize through _program_display/_program_reverse so a raw label can still
resolve to the correct code before falling back to other identifiers like prCode
or program.
custom_components/addhon/diagnostics.py (1)

218-239: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Range-first ordering correctly avoids the enumerated-grid dump.

Good fix — checking param_range() before param_values() prevents the expensive/dangerous grid enumeration for range params, and is covered by the new test. Consider updating the docstring at Line 219 ("value + enum + range + metadata") to reflect that enum is now only emitted as a fallback when no range exists, since the current wording still implies both are always considered independently.

🤖 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 `@custom_components/addhon/diagnostics.py` around lines 218 - 239, The
range-first logic in _param_schema is correct, but the docstring still implies
value, enum, and range are always emitted together. Update the _param_schema
docstring and nearby comment so it reflects the actual behavior: range is
checked first, and enum is only added as a fallback when param_range() returns
None. Keep the explanation aligned with _param_schema, param_range(), and
param_values().
custom_components/addhon/client/transport/connection.py (1)

195-206: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Don't cache CancelledError for sibling reauth flows.
except BaseException also catches asyncio.CancelledError, so if one request is cancelled during create()/authenticate(), that instance gets cached and later re-raised into siblings that were never cancelled. Catch CancelledError separately: advance the generation, but re-raise it without storing it in _reauth_error.

🤖 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 `@custom_components/addhon/client/transport/connection.py` around lines 195 -
206, The reauth retry block in connection handling is caching cancellations as
shared auth failures because it catches BaseException in the
create()/authenticate() path. Update the logic in the reauth flow around
self.create(), self.auth.authenticate(), and the _reauth_error/_reauth_error_gen
assignment so asyncio.CancelledError is handled separately: still bump
_refresh_gen and advance the generation, but re-raise it immediately without
storing it in _reauth_error. Keep the existing caching behavior only for
non-cancellation exceptions so sibling requests do not inherit a cancelled task.
custom_components/addhon/button.py (2)

154-178: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the snapshot/restore rollback helper into a shared module.

The same dict(p.__dict__) snapshot / __dict__.clear()+update() restore pattern is reimplemented independently here, in switch.py's _send_pause_command, and (per the comment on line 160) in hon_commands.async_send_command. Consolidating into one shared helper (e.g. engine/rollback.py) would reduce the risk of the three copies drifting apart on future edge-case fixes.

♻️ Sketch of a shared helper
# e.g. custom_components/addhon/client/engine/param_rollback.py
def snapshot_params(params: dict) -> dict:
    if not isinstance(params, dict):
        return {}
    return {k: dict(p.__dict__) for k, p in params.items() if hasattr(p, "__dict__")}


def restore_params(params: dict, snapshot: dict) -> None:
    if not isinstance(params, dict):
        return
    for key, saved in snapshot.items():
        param = params.get(key)
        if param is not None and hasattr(param, "__dict__"):
            param.__dict__.clear()
            param.__dict__.update(saved)

Also applies to: 191-195, 244-246, 285-285, 322-331

🤖 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 `@custom_components/addhon/button.py` around lines 154 - 178, The rollback
snapshot/restore logic in the button send flow is duplicated and should be
centralized. Extract the shared `dict(p.__dict__)` snapshot and
`__dict__.clear()+update()` restore behavior from this code path, `switch.py`’s
`_send_pause_command`, and `hon_commands.async_send_command` into a single
helper module such as `param_rollback` (or similar), then update the existing
callers to use that helper so future fixes stay consistent.

154-178: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Extract the rollback snapshot/restore helper
This logic is duplicated in custom_components/addhon/hon_commands.py, custom_components/addhon/switch.py, and custom_components/addhon/program_options.py; centralizing it would keep rollback behavior aligned across all send paths.

🤖 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 `@custom_components/addhon/button.py` around lines 154 - 178, Extract the
rollback snapshot/restore logic used in the button send flow into a shared
helper so it can be reused consistently by the send paths in button,
hon_commands.async_send_command, switch, and program_options. Move the
_snapshot_params and _restore_params behavior into a common utility, then have
the existing rollback handling in button.py call that shared helper instead of
keeping a local copy.
🤖 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.

Nitpick comments:
In `@custom_components/addhon/button.py`:
- Around line 154-178: The rollback snapshot/restore logic in the button send
flow is duplicated and should be centralized. Extract the shared
`dict(p.__dict__)` snapshot and `__dict__.clear()+update()` restore behavior
from this code path, `switch.py`’s `_send_pause_command`, and
`hon_commands.async_send_command` into a single helper module such as
`param_rollback` (or similar), then update the existing callers to use that
helper so future fixes stay consistent.
- Around line 154-178: Extract the rollback snapshot/restore logic used in the
button send flow into a shared helper so it can be reused consistently by the
send paths in button, hon_commands.async_send_command, switch, and
program_options. Move the _snapshot_params and _restore_params behavior into a
common utility, then have the existing rollback handling in button.py call that
shared helper instead of keeping a local copy.

In `@custom_components/addhon/client/transport/connection.py`:
- Around line 195-206: The reauth retry block in connection handling is caching
cancellations as shared auth failures because it catches BaseException in the
create()/authenticate() path. Update the logic in the reauth flow around
self.create(), self.auth.authenticate(), and the _reauth_error/_reauth_error_gen
assignment so asyncio.CancelledError is handled separately: still bump
_refresh_gen and advance the generation, but re-raise it immediately without
storing it in _reauth_error. Keep the existing caching behavior only for
non-cancellation exceptions so sibling requests do not inherit a cancelled task.

In `@custom_components/addhon/diagnostics.py`:
- Around line 218-239: The range-first logic in _param_schema is correct, but
the docstring still implies value, enum, and range are always emitted together.
Update the _param_schema docstring and nearby comment so it reflects the actual
behavior: range is checked first, and enum is only added as a fallback when
param_range() returns None. Keep the explanation aligned with _param_schema,
param_range(), and param_values().

In `@custom_components/addhon/select.py`:
- Around line 325-341: Handle the edge case where current_option receives a bare
colliding program label from programName, since _program_reverse now only stores
suffixed display labels for collisions. Update HonProgramOptionSelect’s lookup
logic in current_option to normalize through _program_display/_program_reverse
so a raw label can still resolve to the correct code before falling back to
other identifiers like prCode or program.

In `@custom_components/addhon/sensor.py`:
- Around line 837-838: The repeated coordinator.data dict-guard is duplicated
across the platform entity setup loops, so extract that logic into a shared
helper in base_entity.py or a small utils module and have sensor.py,
binary_sensor.py, and select.py call it instead of inlining the isinstance
check. Keep the helper focused on returning a safe dict from coordinator.data,
and update the setup code that iterates appliance_id/data to use the shared
function so the behavior stays consistent across platforms.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 59726632-d533-4557-b07b-500239483e78

📥 Commits

Reviewing files that changed from the base of the PR and between 17d7ba9 and 97e5f1c.

📒 Files selected for processing (26)
  • custom_components/addhon/__init__.py
  • custom_components/addhon/base_entity.py
  • custom_components/addhon/binary_sensor.py
  • custom_components/addhon/button.py
  • custom_components/addhon/client/engine/appliance.py
  • custom_components/addhon/client/engine/commands.py
  • custom_components/addhon/client/engine/parameter/base.py
  • custom_components/addhon/client/engine/rules.py
  • custom_components/addhon/client/transport/auth.py
  • custom_components/addhon/client/transport/connection.py
  • custom_components/addhon/climate.py
  • custom_components/addhon/config_flow.py
  • custom_components/addhon/const.py
  • custom_components/addhon/diagnostics.py
  • custom_components/addhon/manifest.json
  • custom_components/addhon/select.py
  • custom_components/addhon/sensor.py
  • 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_diagnostics.py
  • tests/test_engine_cluster.py
  • tests/test_options_flow.py
  • tests/test_program_select.py
  • tests/test_transport_connection.py

…ering select case)

- connection.py: don't cache CancelledError in the reauth single-flight. The
  `except BaseException` also caught asyncio.CancelledError and stored it in
  _reauth_error, so a cancellation on one request was re-raised into sibling
  requests that were never cancelled. Handle CancelledError separately: advance
  the generation (create() already reset auth to token-less) but re-raise
  without storing it.
- param_rollback.py (new): shared snapshot_params/restore_params helper for the
  send-path rollback (copy __dict__ directly so rules are not re-fired and
  values/min/max are restored). Route hon_commands, button, switch and
  program_options through it instead of four drifting copies.
- base_entity.py: shared coordinator_data_map() guard; sensor/binary_sensor/
  select now call it instead of inlining the isinstance check.
- diagnostics.py: _param_schema docstring now reflects range-first, enum-as-
  fallback ordering.

Behavior-preserving refactors + one docstring + the CancelledError fix. Full
suite green (1039 passed, 1 skipped). Skipped the select.py current_option
nitpick: CodeRabbit itself flagged it as self-recovering and non-blocking.
@tis24dev
tis24dev merged commit d92d5d1 into main Jul 6, 2026
10 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 7, 2026
This was referenced Jul 24, 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.

2 participants