♻️ Skip redundant energy fetches and emit statistics at real timestamps #18

Open
mat wants to merge 2 commits from fix/5-energy-hour-offset into main
Owner

Summary

Closes MateoGreil/homeassistant-comwatt#3, MateoGreil/homeassistant-comwatt#5, MateoGreil/homeassistant-comwatt#42.

Two related fixes to ComwattEnergySensor bundled because they live in the same code path:

Skip the call when the bucket can't have changed (#3)

The QUANTITY/HOUR endpoint only publishes a new bucket once per hour, so polling every 2 min was 29 wasted calls per hour per device. The coordinator now tracks the last successful fetch time per device and skips the call while the cached result is < 55 min old. Power fetches are unchanged.

Emit statistics at the real bucket timestamp (#5, #42)

Previously the energy value was published under now even though the API bucket represents the previous hour, so the Energy dashboard attributed each reading to the wrong hour. The coordinator now forwards each new hourly bucket to async_add_external_statistics with the real bucket timestamp and a running cumulative sum, under an external statistic id comwatt:<device_id>_total_energy.

  • Existing sensor.<device>_total_energy entity unchanged — users whose Energy dashboard points at it keep working.
  • Parallel statistic comwatt:*_total_energy attributes energy to the correct hour; users can opt into it by adding it to the dashboard.

Guarded on "recorder" in hass.config.components so the coordinator stays usable when recorder is disabled.

Test plan

  • New test_energy_fetch_is_skipped_within_interval — second refresh within the window does not re-call QUANTITY; advancing the cached timestamp triggers the call again.
  • New test_new_energy_buckets_are_pushed_as_external_statistics — two new buckets produce one async_add_external_statistics call with the correct statistic id, cumulative sum, unit, and top-of-hour timestamp.
  • pytest tests/ — 26 passed; ruff + mypy clean.
  • Hands-on: after a few hours on your HA, open Settings → Dashboards → Energy → add a new energy source and check that comwatt:<device>_total_energy appears in the statistic picker. Compare the hour alignment to the existing sensor.*_total_energy entity.
## Summary Closes [MateoGreil/homeassistant-comwatt#3](https://github.com/MateoGreil/homeassistant-comwatt/issues/3), [MateoGreil/homeassistant-comwatt#5](https://github.com/MateoGreil/homeassistant-comwatt/issues/5), [MateoGreil/homeassistant-comwatt#42](https://github.com/MateoGreil/homeassistant-comwatt/issues/42). Two related fixes to `ComwattEnergySensor` bundled because they live in the same code path: ### Skip the call when the bucket can't have changed (#3) The QUANTITY/HOUR endpoint only publishes a new bucket once per hour, so polling every 2 min was 29 wasted calls per hour per device. The coordinator now tracks the last successful fetch time per device and skips the call while the cached result is < 55 min old. Power fetches are unchanged. ### Emit statistics at the real bucket timestamp (#5, #42) Previously the energy value was published under `now` even though the API bucket represents the previous hour, so the Energy dashboard attributed each reading to the wrong hour. The coordinator now forwards each new hourly bucket to `async_add_external_statistics` with the real bucket timestamp and a running cumulative sum, under an external statistic id `comwatt:<device_id>_total_energy`. - Existing `sensor.<device>_total_energy` entity **unchanged** — users whose Energy dashboard points at it keep working. - Parallel statistic `comwatt:*_total_energy` attributes energy to the correct hour; users can opt into it by adding it to the dashboard. Guarded on `"recorder" in hass.config.components` so the coordinator stays usable when recorder is disabled. ## Test plan - [x] New `test_energy_fetch_is_skipped_within_interval` — second refresh within the window does not re-call QUANTITY; advancing the cached timestamp triggers the call again. - [x] New `test_new_energy_buckets_are_pushed_as_external_statistics` — two new buckets produce one `async_add_external_statistics` call with the correct statistic id, cumulative sum, unit, and top-of-hour timestamp. - [x] `pytest tests/` — 26 passed; `ruff` + `mypy` clean. - [ ] Hands-on: after a few hours on your HA, open Settings → Dashboards → Energy → add a new energy source and check that `comwatt:<device>_total_energy` appears in the statistic picker. Compare the hour alignment to the existing `sensor.*_total_energy` entity.
♻️ Skip redundant energy fetches and emit statistics at real timestamps
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m45s
Validate / type-check-mypy (pull_request) Successful in 1m48s
4a14c4d425
Closes #3, #5 and #42.

Two related energy-sensor issues addressed in one pass:

### #3 — "Adjust Update Frequency for ComwattEnergySensor"

The Comwatt QUANTITY/HOUR endpoint publishes a new bucket once per
hour, so polling it every 2 minutes was 29 wasted calls per hour per
device. The coordinator now tracks the last successful fetch time per
device and skips the call while the cached result is less than 55
minutes old. Power fetches are unchanged.

### #5 / #42 — hour offset / "délais de mise à jour"

Previously the energy sensor published the accumulated total under
timestamp `now`, even though the API bucket it came from represents
the **previous** hour. The Energy dashboard therefore attributed each
reading to the wrong hour, and new values only appeared after HA had
caught up with the 2-minute poll cadence.

The coordinator now forwards each new hourly bucket to
`async_add_external_statistics` with the real bucket timestamp and a
running cumulative sum, under an external statistic id
`comwatt:<device_id>_total_energy`. This:

- keeps the existing `sensor.<device>_total_energy` entity unchanged
  (no breaking change for users whose Energy dashboard points at it);
- exposes a parallel statistic attributed to the correct hour that
  users can opt into by adding `comwatt:*_total_energy` to the
  dashboard instead.

The push is guarded on `"recorder" in hass.config.components` so the
coordinator stays usable on the rare setup where the recorder is
disabled.

### Tests

- `test_energy_fetch_is_skipped_within_interval`: a second refresh
  within the window does not re-call QUANTITY; advancing the cached
  `last_fetched_at` triggers the call again.
- `test_new_energy_buckets_are_pushed_as_external_statistics`: two
  new buckets produce one `async_add_external_statistics` call with
  the correct statistic id, cumulative sum, unit, and top-of-hour
  timestamp.
mat force-pushed fix/5-energy-hour-offset from 4a14c4d425
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m45s
Validate / type-check-mypy (pull_request) Successful in 1m48s
to 1b23292466
All checks were successful
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m44s
Validate / type-check-mypy (pull_request) Successful in 1m49s
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 7s
Validate / test-pytest (push) Successful in 1m45s
Validate / type-check-mypy (push) Successful in 1m48s
2026-04-24 21:53:00 +00:00
Compare
mat self-assigned this 2026-04-29 10:03:15 +00:00
🐛 Handle real Comwatt API types in energy statistics
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 10s
Validate / test-pytest (push) Successful in 1m50s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m46s
Validate / type-check-mypy (pull_request) Successful in 1m50s
8d78841b39
The /aggregations/time-series endpoint returns timestamps as ISO 8601
strings (e.g. "2026-04-29T10:00:00.000+0000") and device IDs as ints —
not the seconds-since-epoch ints and string IDs the test fixtures used.
Both shapes broke at runtime on first deploy.

- Add _parse_bucket_ts to normalise timestamps to UTC datetime,
  accepting ISO 8601 (with Z, +0000, +HH:MM, naive), epoch seconds,
  epoch milliseconds, and numeric strings; return None for garbage.
- Compare and store buckets as datetime instead of int, so the dedup
  check no longer mixes types.
- Stringify device_id before slugify; slugify rejects ints with a
  decoding error.
- Cover the parser with edge-case tests so future API drift is caught
  at the boundary.
All checks were successful
Validate / validate-hacs (push) Has been skipped
Validate / validate-hassfest (push) Has been skipped
Validate / lint-ruff (push) Successful in 10s
Validate / test-pytest (push) Successful in 1m50s
Validate / type-check-mypy (push) Successful in 1m48s
Validate / validate-hacs (pull_request) Has been skipped
Validate / validate-hassfest (pull_request) Has been skipped
Validate / lint-ruff (pull_request) Successful in 7s
Validate / test-pytest (pull_request) Successful in 1m46s
Validate / type-check-mypy (pull_request) Successful in 1m50s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/5-energy-hour-offset:fix/5-energy-hour-offset
git switch fix/5-energy-hour-offset

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff fix/5-energy-hour-offset
git switch fix/5-energy-hour-offset
git rebase main
git switch main
git merge --ff-only fix/5-energy-hour-offset
git switch fix/5-energy-hour-offset
git rebase main
git switch main
git merge --no-ff fix/5-energy-hour-offset
git switch main
git merge --squash fix/5-energy-hour-offset
git switch main
git merge --ff-only fix/5-energy-hour-offset
git switch main
git merge fix/5-energy-hour-offset
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
mat/homeassistant-comwatt!18
No description provided.