Skip to content

feat: adopt RefreshPartiallySucceeded/RefreshFailed from givenergy-modbus #125#71

Merged
dewet22 merged 4 commits into
mainfrom
feat/adopt-refresh-partial-exceptions
May 29, 2026
Merged

feat: adopt RefreshPartiallySucceeded/RefreshFailed from givenergy-modbus #125#71
dewet22 merged 4 commits into
mainfrom
feat/adopt-refresh-partial-exceptions

Conversation

@dewet22

@dewet22 dewet22 commented May 29, 2026

Copy link
Copy Markdown
Owner

What this does

Adopts the new poll contract from givenergy-modbus#125. Previously the first register read to time out aborted the whole poll, so one offline battery discarded every reading that did come back. Now refresh()/load_config() raise RefreshPartiallySucceeded (carrying the data that arrived) or RefreshFailed (nothing came back), and this integration handles them so a single flaky device no longer takes the rest of the plant down with it.

This also retires the last consumer of the deprecated refresh_plant() (removed in modbus 3.0).

Behaviour

Coordinator

  • Steady-state partial → serve exc.plant. The dropped device's entities freeze at their last-known register values (the library's cache retains them) and stay available, rather than the whole device going unavailable for the tick. A new cumulative partial_failures counter records it.
  • (Re)connect-seed partial → treated as a failure, not served. There's no per-device fallback on a seed, so I'd rather fail and retry cleanly than lock in a half-populated initial plant. Cold start → ConfigEntryNotReady (HA retries setup); in-process reconnect → the existing tolerance window serves the pre-disconnect snapshot. detect() having already gated device presence is what makes a seed partial most likely transient.
  • RefreshFailed → I kept the existing timeout-tolerance window when every underlying cause is a timeout (transient blip, serve last-known up to the threshold); any non-timeout cause resets the client and fails immediately. This deliberately diverges from the simpler RefreshFailed → UpdateFailed in the modbus PR's migration note, to preserve resilience this integration already had.

Diagnostics — a new Partial Refresh Failures sensor (always-available, like the other coordinator diagnostics). Because a flaky device stays available with stale values, the entity-availability channel stays silent — so the sensor's attributes name the dropping device(s) (0x34, bank, request type) from the structured ReadFailure records. Added to the auto-generated Integration Health card; dashboard schema bumped to v3 (existing installs get the usual Repairs prompt to regenerate).

config_flow_test_connection accepts a partial, since it only needs to identify the inverter (device 0x32), which virtually always survives a peripheral drop. RefreshFailed still maps to cannot_connect.

Testing

  • Full suite green (108 tests); coordinator and config_flow at 100% line coverage, including the partial/total/seed paths and the timeout-vs-non-timeout RefreshFailed split.
  • The partial behaviour is exercised with mocked exceptions rather than a real flaky bus — I haven't yet run the hardware sanity-check (pull one battery's RS485 and watch the entities stay live while the counter ticks). Something I'll do when I next have the kit in front of me.

Follow-up

The interim tradeoff is that a long partial shows a frozen value with no intrinsic "stale" marker — the counter and logs are the only signal. Decisively offlining a single silent device needs per-bank freshness timestamps, tracked in givenergy-modbus#65; the ReadFailure.device_address surfaced here maps straight onto that once it lands.

Dependency

Requires givenergy-modbus>=2.1.0a5 (the release that ships #125); pin bumped in pyproject.toml and manifest.json.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added diagnostic sensor for tracking partial device polling failures, with detailed failure summaries displayed as sensor attributes.
  • Bug Fixes

    • Improved resilience during device polling by distinguishing between complete failures and partial successes, allowing the integration to continue operating with partially-read device data when appropriate.
    • Enhanced reconnection behavior to properly recover partial snapshots instead of discarding incomplete readings.
  • Chores

    • Updated dependency to latest compatible version.

Review Change Stack

…dbus #125

Migrate the coordinator and config flow off the deprecated refresh_plant()
onto load_config()/refresh(), and handle the new partial/total poll
exceptions so one offline device no longer discards every other reading.

- coordinator: steady-state partials serve exc.plant (the dropped device's
  entities freeze at last-known values instead of going unavailable) and bump
  a new partial_failures counter; (re)connect-seed partials fail and retry
  rather than locking in a half-populated initial plant; timeout-driven
  RefreshFailed keeps the existing tolerance window, non-timeout causes reset
  the client immediately
- sensor/dashboard: new Partial Refresh Failures diagnostic sensor whose
  attributes name the dropping device(s) from the ReadFailure records; added
  to the Integration Health card; dashboard schema bumped to v3
- config_flow: _test_connection accepts a partial (the serial survives a
  peripheral drop); RefreshFailed still maps to cannot_connect
- pin givenergy-modbus>=2.1.0a5 (the release that ships #125)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@dewet22, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 40 minutes and 11 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e79cabf8-dd15-4e1e-87c5-65c56c91541a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b0dbae and 4621b9c.

📒 Files selected for processing (6)
  • custom_components/givenergy_local/config_flow.py
  • custom_components/givenergy_local/coordinator.py
  • custom_components/givenergy_local/sensor.py
  • tests/conftest.py
  • tests/test_config_flow.py
  • tests/test_coordinator.py
📝 Walkthrough

Walkthrough

This PR refactors the GivEnergy Home Assistant integration to support graceful handling of partial refresh outcomes. It replaces the refresh_plant(full_refresh=...) API with separate load_config() and refresh() calls, adds tracking of partial failures in the coordinator, and introduces a new diagnostic sensor to report them. All connection logic, update paths, and tests are updated accordingly.

Changes

Partial Refresh Failure Handling

Layer / File(s) Summary
Dependency version updates
manifest.json, pyproject.toml
Bumps givenergy-modbus from 2.1.0a4 to 2.1.0a5 to unlock the new refresh API contract.
Coordinator state and exception setup
custom_components/givenergy_local/coordinator.py
Imports additional exception types and adds partial_failures and last_partial_failures fields to track partial polling degradations alongside existing full-failure counters.
Coordinator update logic and helpers
custom_components/givenergy_local/coordinator.py
Refactors _async_update_data to introduce _mark_success(), _record_failure(), _record_partial(), _is_timeout_failure(), and _serve_last_known_or_fail() helpers; implements distinct reconnect-seed vs steady-state handling for RefreshPartiallySucceeded; standardizes timeout-only failure detection and tolerance-window behavior; adjusts client reset logic.
Active and passive refresh wiring
custom_components/givenergy_local/coordinator.py
Changes _active_update to call load_config() on full-refresh ticks, then always refresh(); updates _passive_update reconnect path to also perform load_config() before refresh(), decoupling config seeding from polling.
Config flow connection test updates
custom_components/givenergy_local/config_flow.py
Imports RefreshPartiallySucceeded and refactors _test_connection to call refresh() instead of refresh_plant(full_refresh=False), extracting the exception's plant snapshot to treat partial success as sufficient.
Sensor definitions and partial failure attributes
custom_components/givenergy_local/sensor.py, custom_components/givenergy_local/dashboard.py
Adds attributes_fn field to GivEnergyCoordinatorSensorDescription, implements _partial_failure_attributes() helper to summarize failed devices and per-failure details, introduces partial_failures diagnostic sensor, adds extra_state_attributes property to GivEnergyCoordinatorSensor, increments dashboard version to 3, and adds "Partial Failures" entity to Integration Health cards.
Test fixture mock API updates
tests/conftest.py
Updates mock_client fixture to define refresh and load_config as AsyncMock instances returning mock_plant, replacing the previous refresh_plant mock.
Config flow test coverage
tests/test_config_flow.py
Adds imports for RefreshFailed and RefreshPartiallySucceeded, introduces tests for partial-success entry creation and full-failure form error display, updates reconfigure test mocks and assertions to target refresh / load_config instead of refresh_plant.
Coordinator test coverage
tests/test_coordinator.py
Adds exception-constructor helpers, refactors timeout/success/error tests to mock refresh side effects, introduces comprehensive partial/total refresh outcome coverage (steady-state serving, cumulative partial counting, cold-seed failure, reconnect-seed last-known fallback), updates passive-mode seeding and reconnect assertions, and validates retry parameter threading through load_config and refresh calls.
Sensor test coverage
tests/test_sensor.py
Updates coordinator-failure setup to trigger TimeoutError via refresh side effect, asserts partial_failures sensor initializes at zero, verifies increment to 1 when RefreshPartiallySucceeded occurs with attributes showing failed device address and failure count.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • dewet22/givenergy-hass#36: Both PRs modify the same integration touchpoints—config_flow._test_connection and the coordinator's refresh/connection workflow—so this PR's switch to refresh()/load_config() and new RefreshPartiallySucceeded handling builds directly on the earlier givenergy-modbus 2.x API adoption.

Poem

A rabbit hops through partial reads,
When some devices fail, the integration still succeeds,
No more all-or-nothing, just graceful decline,
Last known data flows, and the sensors align. 🐰✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.58% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adopting new exception types (RefreshPartiallySucceeded/RefreshFailed) from the givenergy-modbus dependency (#125), which is the central theme across all modified files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adopt-refresh-partial-exceptions

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 and usage tips.

@socket-security

socket-security Bot commented May 29, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedgivenergy-modbus@​2.1.0a4 ⏵ 2.1.0a5100 +1100100100100

View full report

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces support for handling partial refresh successes (via RefreshPartiallySucceeded) in the GivEnergy Local integration, allowing the system to serve last-known data for flaky peripheral devices instead of failing the entire poll. It also adds a new diagnostic sensor to track partial failures. The review feedback points out a critical bug in dashboard.py where the entity key was incorrectly referenced as "partial_refresh_failures" instead of "partial_failures". Additionally, it suggests cleaner handling of attributes_fn by defaulting to None instead of a dummy lambda, and recommends defensively extracting .value from f.request_type in case it is upgraded to an Enum in the future.

Comment thread custom_components/givenergy_local/dashboard.py
Comment thread custom_components/givenergy_local/sensor.py Outdated
Comment thread custom_components/givenergy_local/sensor.py
Comment thread custom_components/givenergy_local/sensor.py
@dewet22-codex

Copy link
Copy Markdown
Collaborator

I reviewed this and do not have any actionable concerns. The new partial/total refresh paths line up with the givenergy-modbus exception contract, the dependency pins are aligned across manifest/pyproject/lock, and the local test suite passes (108 passed).

One residual note: uv run ruff check . still reports the existing scripts/migrate_from_givtcp.py formatting drift, but I did not treat that as part of this PR.

@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/conftest.py (1)

172-177: ⚡ Quick win

Constrain mock_client with spec_set so deprecated client APIs fail fast

tests/conftest.py::mock_client builds a bare AsyncMock(), which auto-synthesizes attributes like client.refresh_plant; that can let regressions slip through without failing the migration tests. Add a spec_set so the mock enforces the intended client surface area (refresh + load_config, plus the other explicitly used methods/attrs).

Possible tightening
 `@pytest.fixture`
 def mock_client(mock_plant) -> AsyncMock:
-    client = AsyncMock()
+    client = AsyncMock(
+        spec_set=[
+            "connected",
+            "plant",
+            "refresh",
+            "load_config",
+            "connect",
+            "detect",
+            "close",
+            "one_shot_command",
+        ]
+    )
     client.connected = True
     client.plant = mock_plant
     client.refresh = AsyncMock(return_value=mock_plant)
     client.load_config = AsyncMock(return_value=mock_plant)
     client.connect = AsyncMock()
     client.detect = AsyncMock()
     client.close = AsyncMock()
     client.one_shot_command = AsyncMock()
🤖 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/conftest.py` around lines 172 - 177, The mock_client fixture currently
creates a freeform AsyncMock() that can auto-synthesize unexpected attributes;
update the mock creation in mock_client to pass a spec_set that matches the real
client API surface you rely on (at minimum include attributes/methods:
connected, plant, refresh, load_config and any other methods/attrs used in
tests) so attempts to access deprecated or misspelled APIs will raise
AttributeError; ensure you construct the AsyncMock with spec_set and then set
client.connected, client.plant and client.refresh/load_config return values as
before.
🤖 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/givenergy_local/config_flow.py`:
- Around line 101-110: When catching RefreshPartiallySucceeded in the config
flow after calling client.refresh(), ensure the partial snapshot actually
contains an inverter serial before returning it: in the except
RefreshPartiallySucceeded as exc branch (and where you set plant = exc.plant)
check if plant and plant.inverter_serial_number are present; if the
inverter_serial_number is missing or falsy, treat it like a full failure (e.g.,
re-raise RefreshFailed or return a value that triggers the existing
"cannot_connect" handling) instead of returning a None/empty unique ID.

In `@custom_components/givenergy_local/coordinator.py`:
- Around line 117-118: On a clean successful refresh, clear any stale
partial-failure diagnostics so old dropped-device details are not reported;
inside the coordinator's success path (e.g., in _mark_success or immediately
where self._mark_success(plant) returns a clean plant) reset the plant's
last_partial_failures tracker (the last_partial_failures attribute on the
plant/state object) to an empty structure or None so prior partial-poll failures
are removed after a full success.

In `@tests/test_coordinator.py`:
- Around line 642-661: In test_active_mode_nth_tick_is_full_refresh replace the
non-ASCII en dash in the loop comment inside the for loop (the "0–10" text) with
an ASCII hyphen ("0-10") so Ruff RUF003 is resolved; update the comment located
next to the for _ in range(11) loop and then run your ruff fix/format commands
before committing.

---

Nitpick comments:
In `@tests/conftest.py`:
- Around line 172-177: The mock_client fixture currently creates a freeform
AsyncMock() that can auto-synthesize unexpected attributes; update the mock
creation in mock_client to pass a spec_set that matches the real client API
surface you rely on (at minimum include attributes/methods: connected, plant,
refresh, load_config and any other methods/attrs used in tests) so attempts to
access deprecated or misspelled APIs will raise AttributeError; ensure you
construct the AsyncMock with spec_set and then set client.connected,
client.plant and client.refresh/load_config return values as before.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 39a74c3c-6ccd-4339-bd0a-376cfe3a60a3

📥 Commits

Reviewing files that changed from the base of the PR and between fae0d2e and 4b0dbae.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • custom_components/givenergy_local/config_flow.py
  • custom_components/givenergy_local/coordinator.py
  • custom_components/givenergy_local/dashboard.py
  • custom_components/givenergy_local/manifest.json
  • custom_components/givenergy_local/sensor.py
  • pyproject.toml
  • tests/conftest.py
  • tests/test_config_flow.py
  • tests/test_coordinator.py
  • tests/test_sensor.py

Comment thread custom_components/givenergy_local/config_flow.py Outdated
Comment thread custom_components/givenergy_local/coordinator.py
Comment thread tests/test_coordinator.py
dewet22 and others added 3 commits May 29, 2026 22:11
- attributes_fn defaults to None (not a dummy lambda); extra_state_attributes
  guards on it, so sensors without attributes skip the call entirely
- defensively extract .value from ReadFailure.request_type in case the library
  later upgrades it from str to an Enum

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback:
- config_flow: a partial that dropped the inverter read itself returns no
  serial → cannot_connect, rather than creating an entry with a blank unique ID
- coordinator: clear last_partial_failures on a fully clean poll so the
  diagnostic sensor stops naming devices that have since recovered (the
  cumulative partial_failures counter is untouched)
- tests for both; ASCII hyphen in a loop comment

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…essions

Per review: the fixture's spec_set omits the deprecated refresh_plant() so any
lingering call fails fast with AttributeError instead of silently auto-mocking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dewet22 dewet22 merged commit 80efc73 into main May 29, 2026
8 checks passed
@dewet22 dewet22 deleted the feat/adopt-refresh-partial-exceptions branch May 29, 2026 21:27
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