♻️ Introduce DataUpdateCoordinator and shared ComwattClient #14
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refactor/coordinator"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Replaces the per-entity sync-polling + cookie-dict-passing pattern with a single
DataUpdateCoordinatorthat owns one long-livedComwattClientper config entry. Addresses findings C2, C4, C6, H5, H7, H8, M3 and M6 structurally; C3 is fixed as a side-effect.coordinator.py: oneComwattClientper entry, every HTTP call runs in the executor, single_fetch_allcycle that walks sites → devices → partChilds and also reads switch state. Energy accumulator state lives on the coordinator. On fetch failure it re-authenticates once and retries; on re-auth 401 it raisesConfigEntryAuthFailed, otherwiseUpdateFailed.__init__.py:entry.runtime_datanow holds the coordinator;async_config_entry_first_refreshmaps setup-time failures to the correct HA exceptions; unload is trivial.sensor.py/switch.py: sensors and switch are nowCoordinatorEntitysubclasses.native_value/is_onare memory-only properties (no I/O, per HA entity contract).availableis gated on coordinator success and the entity's key existing incoordinator.data. Switch turn_on/off are async and delegate the blocking client call to the executor, then request a coordinator refresh.SCAN_INTERVALremoved from both platforms; the coordinator'sUPDATE_INTERVALis the single source of truth. Credentials no longer stored on entity instances.conftest.pypatches the newcoordinator.ComwattClientimport site (plusconfig_flow.ComwattClientunchanged).test_init.pyassertsentry.runtime_datais aComwattCoordinator. Newtest_coordinator.pycovers three error paths: bad-credential 401 → not loaded, transient 5xx →SETUP_RETRY, one-off failure recovered by re-auth + retry.Unique ids and
device_infoidentifiers are unchanged ({device_id}_power,{device_id}_total_energy,{device_id}_switch,site_{site_id}_auto_production_rate; identifiers still(DOMAIN, device['name'])). Existing users' entity history and device registry entries keep working. H1/H2 will be addressed in dedicated PRs with a proper migration.Test plan — please validate on your HA before merging
This is the first PR that changes runtime behavior, so per our agreed workflow it needs hands-on verification against the real Comwatt backend. Suggested checklist:
custom_components/comwatt/over your HA config, restart.KeyErrorin the log.UpdateFailedduring transient API blips is normal; a re-auth loop is not.Followups
ConfigEntryAuthFailedcorrectly, but HA needs anasync_step_reauthto actually collect new credentials.Test status locally
pytest tests/— 24 passedruff check .— cleanmypy— clean (existingcustom_components.comwatt.*ignore untouched; this PR doesn't narrow it because there are still untyped findings elsewhere — later cleanup PR)Replaces the per-entity sync-polling + cookie-dict passing pattern with a single `DataUpdateCoordinator` that owns one long-lived `ComwattClient` for the lifetime of the config entry. Addresses findings C2, C4, C6, H5, H7, H8, M3 and M6 in one structural pass. ## Changes - New `coordinator.py` with `ComwattCoordinator`: - One `ComwattClient` per entry, kept across polls so session cookies and connection pool survive. - `_async_update_data` runs every HTTP call inside `async_add_executor_job`, so nothing blocks the event loop. - A single `_fetch_all` cycle walks sites → devices → (optional) partChilds, collects power/energy/auto-production-rate metrics, and also refreshes switch state for switchable devices. Topology discovery no longer duplicated between `sensor.py` and `switch.py`. - Energy accumulator state (last_ts, total) lives on the coordinator keyed by device id instead of being sprinkled across entity instances. - On any fetch exception the coordinator re-authenticates once and retries; a 401/403 on re-auth raises `ConfigEntryAuthFailed`, other failures map to `UpdateFailed`. - `__init__.py`: - `entry.runtime_data` now holds the coordinator directly. - `async_config_entry_first_refresh` turns setup-time failures into the right `ConfigEntryAuthFailed` / `ConfigEntryNotReady` for HA. - `hass.data[DOMAIN]` is no longer used; unload is a one-liner. - `sensor.py` and `switch.py`: - All three sensors and the switch now subclass `CoordinatorEntity[ComwattCoordinator]` and read from `coordinator.data`. `native_value` and `is_on` are properties that only look at memory — no I/O per the HA entity contract. - `available` is gated on coordinator success AND the entity's key being present in `coordinator.data` (so a device that disappeared on the next poll goes unavailable instead of keeping a stale value). - Switch turn_on/turn_off are now async; they delegate the blocking `client.switch_capacity` to the executor via the coordinator and then request a refresh so the UI settles on the real server state. - Local `SCAN_INTERVAL` constants removed from both platforms; the coordinator's `UPDATE_INTERVAL` is the single source of truth. - Credentials are no longer stored on entity instances. - Tests: - `conftest.py` patches `coordinator.ComwattClient` and the existing `config_flow.ComwattClient` — those are the only two import sites now. - `test_init.py` asserts `entry.runtime_data` is a `ComwattCoordinator` after setup. - `test_sensor.py`'s energy test updated for the new `float` representation (coordinator accumulator starts at `0.0`). - New `test_coordinator.py` covers the three error paths: bad-credential 401 → not loaded, transient upstream 5xx → SETUP_RETRY, one-off failure recovered by a re-auth + retry cycle. ## Findings addressed - C2: blocking I/O on the event loop → coordinator uses executor. - C4: `ComwattClient` rebuilt per call with a cookie-dict hack → one shared client per entry. - C6: `async_setup_entry` had no error handling → proper `ConfigEntryAuthFailed` / `ConfigEntryNotReady` mapping. - H5: bare `except Exception` that forced a silent re-auth on *every* error → narrow to a single re-auth + retry path in the coordinator, with distinct `ConfigEntryAuthFailed` vs `UpdateFailed` outcomes. - H7: sync `turn_on` / `turn_off` → `async_turn_on` / `async_turn_off` using executor for the blocking API call. - H8: `SCAN_INTERVAL` redefined in `sensor.py` and `switch.py` → coordinator owns the interval. - M3: topology walk duplicated across platforms → single implementation in `ComwattCoordinator._fetch_all`. - M6: no `available` fallback → coordinator-driven availability. ## Findings deliberately NOT addressed in this PR - C3 (credentials duplicated on entity instances) → **done** as a side-effect of moving them onto the coordinator. - C7 (reauth flow): still no `async_step_reauth`; the coordinator raises `ConfigEntryAuthFailed` but HA needs the flow to actually collect new credentials from the user. Tracked as a follow-up PR. - H1 (device_info identifiers use device name) / H2 (unique_ids not domain-namespaced) / H3 (`has_entity_name`) / H4 (energy persistence) / H6 (duplicate entry) / L1 (explicit `runtime_data` type alias) / L7 (translations) / M7 (manifest metadata): unchanged, each gets its own PR.915bf1ce4d61a61b16e5