Skip to content

Adopt givenergy-modbus 2.x APIs; add device services; misc fixes#36

Merged
dewet22 merged 7 commits into
mainfrom
next
May 14, 2026
Merged

Adopt givenergy-modbus 2.x APIs; add device services; misc fixes#36
dewet22 merged 7 commits into
mainfrom
next

Conversation

@dewet22
Copy link
Copy Markdown
Owner

@dewet22 dewet22 commented May 14, 2026

Summary

Batches up the work that's been accumulating on `next` since the last merge to `main`. All seven commits land together because they sit on a single rebased line and several of them depend on each other (the library bump, the alias rename and the slot-command refactor in particular).

  • library: bump dependency spec to `givenergy-modbus>=2.0.0,<3.0.0`, point the source at the GitHub `main` branch while v2 is still being stabilised
  • 2.x adoption: call `Client.detect()` once on connect so `refresh_plant()` dispatches via the new model-aware `load_config()`/`refresh()` path; introduce a shared `InverterModel = SinglePhaseInverter | ThreePhaseInverter` alias so three-phase, AIO-HV and EMS topologies now flow through type-correctly
  • slot commands: switch the time platform from the deprecated `set_charge_slot_1/2` / `set_discharge_slot_1/2` wrappers to `commands.set_charge_slot(idx, ts, slot_map)` / `set_discharge_slot(...)`, driven by `plant.inverter.slot_map` (auto-selects single-phase / extended 10-slot / three-phase layouts per model)
  • max_batteries removal: the `capabilities.lv_battery_addresses` discovered by `detect()` drives battery enumeration now, so the old `CONF_MAX_BATTERIES` field, schema entry, coordinator parameter and translations are dropped. Existing config entries keep working — the residue key is just ignored
  • services: expose `reboot_inverter` and `calibrate_battery_soc` as domain services (Developer Tools only — deliberately not button entities so they can't be triggered accidentally from a dashboard), with mandatory `device_id`
  • resilience: reset the client after timeout tolerance is exceeded so the next tick gets a fresh TCP connection

Test plan

  • `uv run pytest` — 70 passing
  • `uv run ruff check` + `uv run ruff format` clean
  • pre-commit hooks pass (mypy, bandit, codespell, …)
  • smoke-test against a real single-phase inverter — config flow / detect / refresh / set a charge slot
  • verify Developer Tools shows the two new services and they no-op gracefully if no device is selected

Summary by CodeRabbit

  • New Features

    • Added device services: Reboot Inverter and Calibrate Battery SOC
    • Improved device detection to better discover topology and capabilities
  • Chores

    • Upgraded underlying library requirement to the 2.x series
    • Removed manual "number of batteries" configuration (now automatic)
    • Improved timeout tolerance and client-reconnect behavior for greater stability
  • Localization

    • Removed obsolete "Number of Batteries" field from setup UI strings

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c9c71b-f3df-4520-83f8-ba257e7d2ab6

📥 Commits

Reviewing files that changed from the base of the PR and between e68adb9 and 80a2e40.

📒 Files selected for processing (2)
  • custom_components/givenergy_local/__init__.py
  • tests/test_coordinator.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • custom_components/givenergy_local/init.py
  • tests/test_coordinator.py

📝 Walkthrough

Walkthrough

Integration updated for givenergy-modbus 2.x: removed max_batteries setting, introduced coordinator-level InverterModel type, added two device services (reboot_inverter, calibrate_battery_soc) that resolve a coordinator by Home Assistant device_id, refactored time-slot commands, and improved detection/timeout behavior.

Changes

Core integration and services

Layer / File(s) Summary
Integration startup, coordinator wiring, services
custom_components/givenergy_local/__init__.py, custom_components/givenergy_local/const.py, custom_components/givenergy_local/services.yaml
Remove max_batteries from setup; add SERVICE_DEVICE_SCHEMA and _coordinator_for_device(hass, device_id); register device services reboot_inverter and calibrate_battery_soc that look up the coordinator by device and issue one-shot Modbus commands; unregister services on final unload.
Coordinator behavior and connection flow
custom_components/givenergy_local/coordinator.py
Remove max_batteries constructor arg; add InverterModel type alias; call client.detect() during connect; simplify refresh_plant calls to full_refresh only; change TimeoutError handling to keep serving cached data until tolerance exceeded then reset client.
Config flow and manifest/deps
custom_components/givenergy_local/config_flow.py, custom_components/givenergy_local/manifest.json, pyproject.toml
Remove CONF_MAX_BATTERIES from config schema and strings; call client.detect() before refresh_plant(full_refresh=False) in connection test; bump givenergy-modbus requirement to >=2.0.0,<3.0.0 and add uv source override.
Entity API/type updates
custom_components/givenergy_local/number.py, select.py, sensor.py, switch.py
Replace direct givenergy_modbus Inverter typing with coordinator-provided InverterModel across entity description callables and adjust imports.
Time slot refactor and command wiring
custom_components/givenergy_local/time.py
Replace per-description set_slot_cmd with is_charge and slot_index fields; update TIME_DESCRIPTIONS and refactor GivEnergyTimeEntity.async_set_value to choose commands.set_charge_slot vs commands.set_discharge_slot at runtime and call client.one_shot_command with (slot_index, new_slot, inverter.slot_map).
Tests and fixtures
tests/conftest.py, tests/test_config_flow.py, tests/test_coordinator.py
Remove max_batteries from fixtures and test inputs; add mocked client.detect; update coordinator construction and refresh assertions to use full_refresh only; expand timeout-tolerance tests to cover preserved cached-data vs client reset behavior.
Localization and service metadata
custom_components/givenergy_local/strings.json, custom_components/givenergy_local/translations/en.json, custom_components/givenergy_local/services.yaml
Remove max_batteries labels from strings/translations; add service metadata entries for the two new device services.
sequenceDiagram
  participant HA as "Home Assistant" color rgba(100,149,237,0.5)
  participant DeviceReg as "Device Registry" color rgba(60,179,113,0.5)
  participant Coord as "GivEnergy Coordinator" color rgba(255,165,0,0.5)
  participant Modbus as "givenergy-modbus Client" color rgba(199,21,133,0.5)
  participant Inverter as "Inverter" color rgba(70,130,180,0.5)

  HA->>DeviceReg: service call with device_id
  DeviceReg-->>HA: return device -> config_entry_id
  HA->>Coord: lookup coordinator by config_entry_id
  Coord->>Modbus: ensure connected
  Modbus-->>Coord: connected
  Coord->>Modbus: run detect()
  Modbus-->>Coord: topology/capabilities
  Coord->>Modbus: one_shot_command(reboot/calibrate) -> inverter
  Modbus->>Inverter: execute command
  Inverter-->>Modbus: ack/result
  Modbus-->>Coord: return result
  Coord-->>HA: return or raise HomeAssistantError
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I hopped through wires and modbus trees,
I found the slots and read the keys.
Max batteries slipped away,
Services wake the inverter today —
Topology found, the signals sing!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.26% 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 summarizes the main changes: adopting givenergy-modbus 2.x APIs, adding device services, and miscellaneous fixes, all of which are evident in the raw summary.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch next

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

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

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 updates the integration to support givenergy-modbus v2.0.0, replacing the manual max_batteries configuration with automatic topology detection via client.detect(). It also introduces new services for rebooting the inverter and calibrating battery SOC, and refines time slot management. Feedback highlights a critical issue where the _reset_client method is called but not defined in the coordinator, and suggests improving encapsulation and error handling by avoiding direct access to the private _client attribute in service handlers and entities.

Comment thread custom_components/givenergy_local/coordinator.py
Comment thread custom_components/givenergy_local/__init__.py Outdated
Comment thread custom_components/givenergy_local/time.py
dewet22 and others added 6 commits May 14, 2026 23:24
Points uv at the local givenergy-modbus checkout while v2.0.0 is in
development and not yet published to PyPI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches from a local editable path to tracking the upstream git repo
so the next branch always reflects the latest givenergy-modbus main.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
givenergy-modbus renamed Inverter to SinglePhaseInverter; the alias
still works but emits a deprecation warning and will be removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dangerous one-shot commands are registered as domain services rather
than button entities so they only appear in Developer Tools and cannot
be accidentally triggered from a dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents accidental multi-inverter firing by resolving a required
device_id field to a single coordinator via the device registry.
Services.yaml gains a device selector filtered to this integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Call client.detect() once on connect (coordinator + config flow) so
  refresh_plant() dispatches via the new model-aware load_config()/refresh()
  path. This unlocks support for three-phase, AIO-HV, EMS and other
  non-default topologies.
- Replace the deprecated Inverter import with a shared
  InverterModel = SinglePhaseInverter | ThreePhaseInverter alias defined
  in coordinator.py.
- Rebuild the time platform around commands.set_charge_slot / set_discharge_slot
  using plant.inverter.slot_map, replacing the eight deprecated
  set_charge_slot_N / set_discharge_slot_N wrappers.
- Remove the max_batteries config option entirely — capabilities.lv_battery_addresses
  drives battery enumeration now, so the user-facing field, schema entry,
  coordinator parameter and translations are all dead weight.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
pyproject.toml (1)

26-27: ⚡ Quick win

Pin the git source to a commit (or tag) for reproducible and explicit dependency resolution.

While uv.lock currently pins the commit, the source in pyproject.toml should explicitly reference a commit SHA or stable tag. Relying on branch = "main" means dependency resolution can drift day-to-day when the lock file is updated, and the manifest itself doesn't reflect the actual pinned version being used.

Suggested change
 [tool.uv.sources]
-givenergy-modbus = { git = "https://github.com/dewet22/givenergy-modbus", branch = "main" }
+givenergy-modbus = { git = "https://github.com/dewet22/givenergy-modbus", rev = "04ef415d2bb3f019aa436c86324803c0f7109713" }
🤖 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 `@pyproject.toml` around lines 26 - 27, The dependency entry under
[tool.uv.sources] for givenergy-modbus should be pinned to a specific commit SHA
or tag instead of branch = "main"; update the givenergy-modbus source to
reference a stable rev (e.g., rev = "<COMMIT_SHA>" or tag = "<vX.Y.Z>") in
pyproject.toml so the manifest is explicit and reproducible, then
regenerate/update uv.lock to capture the new pinned version; locate the entry
named givenergy-modbus in the [tool.uv.sources] section to make this change.
tests/test_coordinator.py (1)

12-15: ⚡ Quick win

Add assertion for detect() call to regression-protect topology discovery on first connect.

The test currently verifies connect() and refresh_plant() but omits detect(), which is called in the coordinator's _connect() method (line 168 of coordinator.py). Since detect() populates plant.capabilities and gates model-aware refresh behavior required for multi-phase and other non-standard topologies, this assertion closes a regression-protection gap.

Suggested test addition
 async def test_first_refresh_connects_and_fetches(hass, mock_client, setup_integration):
     mock_client.connect.assert_called_once()
+    mock_client.detect.assert_called_once()
     mock_client.refresh_plant.assert_called_once_with(full_refresh=True)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_coordinator.py` around lines 12 - 15, The test
test_first_refresh_connects_and_fetches currently asserts connect() and
refresh_plant() but misses asserting that detect() was invoked during the
coordinator's _connect() flow; update the test to include an assertion like
mock_client.detect.assert_called_once() (or
mock_client.detect.assert_called_once_with() if you expect specific args) to
ensure topology discovery is exercised—look for the mock_client usage in the
test and add the detect() assertion alongside the existing connect and
refresh_plant assertions.
🤖 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/__init__.py`:
- Around line 59-67: Both service handlers (handle_reboot_inverter and
handle_calibrate_battery_soc) currently silently no-op when
_coordinator_for_device returns None or the client's disconnected; change them
to fail fast by checking the result of _coordinator_for_device and
c._client.connected and raising a HomeAssistantError (from
homeassistant.exceptions.HomeAssistantError) when the device_id is invalid,
coordinator is missing, or client not connected, including the device_id in the
error message so the caller sees the failure instead of a silent success; keep
the existing await c._client.one_shot_command(...) path when checks pass.

---

Nitpick comments:
In `@pyproject.toml`:
- Around line 26-27: The dependency entry under [tool.uv.sources] for
givenergy-modbus should be pinned to a specific commit SHA or tag instead of
branch = "main"; update the givenergy-modbus source to reference a stable rev
(e.g., rev = "<COMMIT_SHA>" or tag = "<vX.Y.Z>") in pyproject.toml so the
manifest is explicit and reproducible, then regenerate/update uv.lock to capture
the new pinned version; locate the entry named givenergy-modbus in the
[tool.uv.sources] section to make this change.

In `@tests/test_coordinator.py`:
- Around line 12-15: The test test_first_refresh_connects_and_fetches currently
asserts connect() and refresh_plant() but misses asserting that detect() was
invoked during the coordinator's _connect() flow; update the test to include an
assertion like mock_client.detect.assert_called_once() (or
mock_client.detect.assert_called_once_with() if you expect specific args) to
ensure topology discovery is exercised—look for the mock_client usage in the
test and add the detect() assertion alongside the existing connect and
refresh_plant assertions.
🪄 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: a0f2cda3-0f20-453b-b4be-791c2b58b155

📥 Commits

Reviewing files that changed from the base of the PR and between 79110b7 and 2fe86c8.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • custom_components/givenergy_local/__init__.py
  • custom_components/givenergy_local/config_flow.py
  • custom_components/givenergy_local/const.py
  • custom_components/givenergy_local/coordinator.py
  • custom_components/givenergy_local/manifest.json
  • custom_components/givenergy_local/number.py
  • custom_components/givenergy_local/select.py
  • custom_components/givenergy_local/sensor.py
  • custom_components/givenergy_local/services.yaml
  • custom_components/givenergy_local/strings.json
  • custom_components/givenergy_local/switch.py
  • custom_components/givenergy_local/time.py
  • custom_components/givenergy_local/translations/en.json
  • pyproject.toml
  • tests/conftest.py
  • tests/test_config_flow.py
  • tests/test_coordinator.py
💤 Files with no reviewable changes (2)
  • custom_components/givenergy_local/strings.json
  • custom_components/givenergy_local/translations/en.json

Comment thread custom_components/givenergy_local/__init__.py Outdated
Both reboot_inverter and calibrate_battery_soc previously silently
no-op'd if the device_id was invalid, the coordinator was missing,
or the Modbus client was disconnected — the call appeared to succeed
while no command was actually sent.

Also add a detect() assertion to the first-refresh test so the
topology-discovery step doesn't quietly regress.
@dewet22 dewet22 merged commit 6860717 into main May 14, 2026
6 checks passed
@dewet22 dewet22 deleted the next branch May 14, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant