Skip to content

feat: DP price-guarantee sensor + get_heartbeat_metrics service - #10

Merged
mrebbert merged 5 commits into
mainfrom
feature/dp-guarantee-and-hb-metrics
Aug 5, 2026
Merged

feat: DP price-guarantee sensor + get_heartbeat_metrics service#10
mrebbert merged 5 commits into
mainfrom
feature/dp-guarantee-and-hb-metrics

Conversation

@mrebbert

@mrebbert mrebbert commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Surfaces two additive v0.1.44 SDK endpoints (get_subscriptions, get_heartbeat_prices) — one as a static sensor, one as an on-demand service — while keeping marketing-composite risk contained.

  • Sensor sensor.<sys>_dynamic_pulse_price_guarantee — only created when the account has a DYNAMIC_PULSE contract with a populated price_guarantee_value. Value is normalized to EUR/kWh (SDK returns ct/kWh). Version string exposed as attribute for automations that need to key on the guarantee scheme. Semantic caveat spelled out in the class docstring: SDK doesn't document what the guarantee bounds; empirically it matches the flat grid-cost portion of the all-in price, not a max total price — treat as "compare, then wire".

  • Service onekommafive.get_heartbeat_metrics(window) — on-demand fetch of system.get_heartbeat_prices(). Window enum: day | week | month | half_year | year. Response is a flat dict with all ~20 populated HeartbeatPriceWindow fields (PV/grid kWh, tariffs, €-amounts, VAT, plus the cloud's own should_report_implausible_pv_and_feed_in flag). SupportsResponse.ONLY. Returns {available: false} when SDK returns None for the requested window.

  • Refactor: _resolve_config_entry helper extracted from refresh_now; both services now share entry resolution with identical multi-system semantics (HomeAssistantError paths for no-entry / unknown-entry / ambiguous).

Target release: v0.1.53 (2026-08-16 cadence). v0.1.52 is already armed for 2026-08-09; this doesn't scope-creep into it.

Test plan

  • .venv/bin/pytest -q — 224 passing (214 baseline + 6 sensor tests + 4 service tests)
  • .venv/bin/mypy — no issues in 18 source files
  • .venv/bin/ruff check custom_components — clean
  • Live-deployed on homie, integration loaded, no tracebacks
  • Sensor state = 0.12 EUR/kWh (12 ct → ÷100), unit correct, state_class=measurement, version attribute present
  • refresh_now behaviour preserved (helper extraction refactor)
  • Service tested live for day / month / year — real data returned per window; the audited install shows should_report_implausible_pv_and_feed_in: true for the year window (cloud self-flags)
  • Invalid window value rejected by schema (unit-tested only, not live-verified)
  • v0.1.53 release-notes drafted post-merge, following release-notes-scope rule

🤖 Generated with Claude Code

https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN

Markus Rebbert and others added 5 commits August 4, 2026 21:31
Surfaces two v0.1.44 SDK endpoints (get_subscriptions,
get_heartbeat_prices) — one as a sensor, one as an on-demand service —
keeping the marketing-composite risk contained per
feedback-marketing-composite-detection.

## Sensor: `dynamic_pulse_price_guarantee`

Static value captured once at setup from get_subscriptions(customer_id).
Only instantiated when the account has a DYNAMIC_PULSE subscription with
a populated price_guarantee_value. Value normalized to EUR/kWh
(ct/kWh ÷ 100). state_class=measurement, no device_class
(monetary+measurement is HA-invalid). Version string exposed as attribute.

Semantic caveat spelled out in class docstring: SDK doesn't document
what the guarantee bounds; empirically the magnitude matches the flat
grid-cost component (not a max total price). "Compare, then wire" advice.

## Service: `onekommafive.get_heartbeat_metrics(window)`

On-demand fetch of system.get_heartbeat_prices(). Window enum:
day/week/month/half_year/year. Response is a flat dict with all
populated HeartbeatPriceWindow fields (~20 per window: PV/grid kWh,
tariffs, €-amounts, VAT, plus marketing composites the caller can
choose to ignore). Registered SupportsResponse.ONLY. Returns
{"window": ..., "available": false} when SDK returns None for the
requested window.

Also: extracted `_resolve_config_entry` helper from `refresh_now` — both
services share entry resolution with identical multi-system semantics
(HomeAssistantError paths for no-entry / unknown-entry / ambiguous).

## Translations

8 files: sensor name + service (name, description, field labels).

## Tests

- conftest: mock_system_factory gains `subscriptions` and
  `heartbeat_prices` kwargs (both default to safe empty stubs).
- test_price_guarantee_sensor.py NEW: 6 cases (ct/kWh conversion,
  EUR/kWh passthrough, no DP → no sensor, empty guarantee → no sensor,
  missing customer_id → no sensor, endpoint failure → no sensor).
- test_get_heartbeat_metrics_service.py NEW: 4 cases (populated window,
  each window dispatches correctly, invalid window schema-rejected,
  absent window returns {available: false}).

## Live-verified on homie

- Sensor state = 0.12 EUR/kWh, unit = EUR/kWh, state_class = measurement,
  version attribute = "DE_PRICE_GUARANTEE_V2", DE name "1k5 DP-Preisgarantie"
- Service tested for day / month / year — real data returned per window,
  including the `should_report_implausible_pv_and_feed_in` flag (true
  for the year window on Markus' install — cloud self-flags).
- Integration loaded, no tracebacks.

## Local gates

224 tests · mypy clean · ruff clean.

## Release

Targets v0.1.53 (2026-08-16 cadence). v0.1.52 is already armed for
2026-08-09, this doesn't scope-creep into it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
Cleanup pass on the feature branch after audit — five findings, each
justified by concrete payoff, verified live on homie.

## services.py — dedup entry-resolution

_resolve_window_inputs hand-copied the 4-branch config-entry cascade
that _resolve_config_entry already encapsulates. Swap for the helper;
_resolve_config_entry now covers get_cheapest_window,
get_most_expensive_window, refresh_now and get_heartbeat_metrics
uniformly.

## services.py — drop dir(win) reflection

get_heartbeat_metrics handler used `for attr in dir(win): if not
callable...` which reads as AI slop and quietly breaks the day the SDK
adds a helper method to HeartbeatPriceWindow. Swap for
`{k: v for k, v in dataclasses.asdict(win).items() if k != "raw"}`.
Bonus: response field order now follows the SDK model, not
alphabetical noise.

Test file switched from MagicMock-with-spec_set to real
HeartbeatPriceWindow instances (which is what asdict requires); a
one-shot `_win(**overrides)` helper fills unset fields with None.

## coordinator.py — delete dead fields

current_price_with_grid_costs + grid_prices had zero readers outside
their definitions (grep-verified). Removed both PriceData fields, the
tomorrow-side grid_prices.update, and the redundant second
get_current_price call. Behavior unchanged.

## __init__.py + sensor_entities.py — trim docstring bloat

PriceGuarantee dataclass had a 4-line docstring for 2 fields;
_extract_price_guarantee had a 6-line docstring re-stating its
signature; OneKomma5DynamicPulsePriceGuaranteeSensor had a 15-line
"interpretation caveat" duplicating what the smaller version now says
in 3. All three cut to one-to-three lines with the substantive
"empirical magnitude matches grid-cost component" warning preserved.
Also dropped `# pragma: no cover - defensive` comments — log lines
carry the intent.

## sensor_entities.py — extract _DescriptionValueSensor mixin

OneKomma5LiveSensor, OneKomma5WeatherSensor and OneKomma5OptimizationSensor
hand-rolled the same `native_value = description.value_fn(data)` with
null-guard; Optimization also duplicated extra_state_attributes via
attr_fn. Extract a mixin that provides both properties (attr_fn is
optional). Constructors stay per-class since each entity base takes
different __init__ args.

OneKomma5PriceSensor keeps its bespoke setup (forecast + breakdown
+ quarter-hour update on the current-price key is real added
behavior). OneKomma5EVSensor stays bespoke — different base with the
_get_ev() indirection pattern.

## Verification

- Local: 224 pytest passing, mypy clean, ruff clean
- Live on homie:
  - Integration loaded, zero tracebacks
  - Live: pv_leistung reports
  - Price: aktueller_strompreis = 0.3908 EUR/kWh
  - Weather: sonnenscheindauer_heute = 805.7 min
  - Optimization: letzte_optimierungsentscheidung = "battery_no_discharge"
  - DP price-guarantee = 0.12 EUR/kWh (unchanged)
  - get_heartbeat_metrics day returns all 22 populated fields in
    dataclass field order

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
## #6 — trim _price_breakdown docstring
Was: 6-line essay explaining the layering formula.
Now: 2-line WHY note kept (why grid_costs_total is intentionally not surfaced),
formula gone (identifier + code layout say it).

## #7 — dt_util.start_of_local_day() instead of 6-line .replace()
Was: `now_local.replace(year=..., month=..., day=..., hour=0, ...)` +
`start.replace(hour=23, minute=59, second=59, microsecond=999999)`.
Now: `start_of_local_day() + timedelta(days=1)` + `... + timedelta(days=1, microseconds=-1)`.
Same values, one line each.

## #8 — hoist hot-path late imports in coordinator.py
- `from onekommafive.errors import ApiError` was re-imported inside every
  coordinator failure's `except Exception:` block. Hoisted to module scope
  and swapped the `if isinstance(err, ApiError)` pattern for a proper
  `except ApiError:` clause ahead of the generic `except Exception:`.
- `from homeassistant.helpers import issue_registry as ir` + `from .const
  import DOMAIN` were re-imported every EMS-repair-issue evaluation
  (fires on the live coordinator's every refresh). Hoisted; DOMAIN was
  already imported at module scope, ir now is too.

## #9 — trim redundant arithmetic-explaining comments
- Live coord `_EMS_FAILURE_THRESHOLD`: 3-line "5 × 30 s = 2.5 min" arithmetic
  → 2-line "ride out a couple of transient blips" WHY.
- Optimization coord `_last_fired_from_time`: 3-line explanation
  → 1-line "None on the first refresh primes without firing" (load-bearing WHY).

## #11 — skipped
SDK has no OptimizationDecision enum to derive from — decision is a plain
string field on OptimizationEvent. Existing hardcoded tuple + translation-
key-rules comment is already load-bearing WHY; no cleaner refactor available.

## Verification

- Local: 224 tests · mypy clean · ruff clean
- Live on homie:
  - Integration loaded, error_log clean
  - Live: pv_leistung reports (0 W night)
  - Price: aktueller_strompreis 0.3908 EUR/kWh
  - Tomorrow window: 2026-08-05T11:15:00+00:00 (refactored date math)
  - Optimization: battery_no_discharge

Net: coordinator.py trimmed, sensor_entities.py trimmed, no behavior change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
…-metrics service

README additions:
- New "Dynamic-Pulse price guarantee" blurb inside the Dynamic-electricity-
  pricing section — explains sensor scope, unit normalization (ct→EUR/kWh),
  version attribute, and the "compare first, wire second" caveat.
- New `onekommafive.get_heartbeat_metrics` service block after `refresh_now`
  under Services & bus events — schema, response shape, example automation
  comparing cloud vs local monthly grid figures.
- New FAQ entry "Is dynamic_pulse_price_guarantee the max price I'll pay?"
  answers "no, and 1KOMMA5° doesn't document what it bounds" honestly with
  the empirical grid-cost-component interpretation.

CHANGELOG [Unreleased] populated:
- Added: DP price-guarantee sensor + get_heartbeat_metrics service.
- Changed: post-audit refactor sweep summary (net -90 lines, no behavior
  change, tests still pass).

release-notes/v0.1.53.md deliberately deferred to the pre-cadence prep
step (Sunday 2026-08-16 close), so this commit only contains what should
land on main pre-release. No manifest bump — v0.1.52 is still armed
for the imminent cadence.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
The guarantee is bound to 1KOMMA5°-side terms this integration doesn't
(and can't) model, so overselling its dashboard usefulness would just
generate wrong-expectations bug reports. Trim README pricing-section
paragraph + FAQ answer + CHANGELOG entry to say: raw value in EUR/kWh,
informational, don't wire "current ≤ guarantee" automations on it.

Sensor code and tests unchanged — the value is there for users who
know what to compare it against.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
@mrebbert
mrebbert merged commit 6f2c9f5 into main Aug 5, 2026
8 checks passed
@mrebbert
mrebbert deleted the feature/dp-guarantee-and-hb-metrics branch August 5, 2026 04:26
mrebbert pushed a commit that referenced this pull request Aug 7, 2026
…v0.1.52

PR #10 merged post-cadence-prep — instead of deferring to v0.1.53
(2026-08-16) we bundle both new surfaces into the imminent 2026-08-09
cadence.

CHANGELOG: [Unreleased] entries moved into [0.1.52], the two ### Changed
sections merged, "not yet consumed" caveat on the SDK-bump line dropped
(the new sensor + service DO consume the new endpoints).

release-notes/v0.1.52.md rewritten to advertise:
- Bus event onekommafive_notification (existing)
- Service get_heartbeat_metrics(window) — with cloud-vs-local example
- Sensor dynamic_pulse_price_guarantee — informational, T&Cs caveat
- Diagnostic diag_notification_update (existing)
- refresh_now(notifications) scope change (existing)
- SDK pin >=0.1.44 (now with corrected "consumed by ..." framing)

No manifest bump — 0.1.52 stays; cadence Sunday 2026-08-09 20:00 UTC
packages what's on main at that moment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN
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