feat: DP price-guarantee sensor + get_heartbeat_metrics service - #10
Merged
Conversation
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 populatedprice_guarantee_value. Value is normalized toEUR/kWh(SDK returnsct/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 ofsystem.get_heartbeat_prices(). Window enum:day | week | month | half_year | year. Response is a flat dict with all ~20 populatedHeartbeatPriceWindowfields (PV/grid kWh, tariffs, €-amounts, VAT, plus the cloud's ownshould_report_implausible_pv_and_feed_inflag).SupportsResponse.ONLY. Returns{available: false}when SDK returnsNonefor the requested window.Refactor:
_resolve_config_entryhelper extracted fromrefresh_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— clean0.12 EUR/kWh(12 ct → ÷100), unit correct, state_class=measurement, version attribute presentrefresh_nowbehaviour preserved (helper extraction refactor)day/month/year— real data returned per window; the audited install showsshould_report_implausible_pv_and_feed_in: truefor the year window (cloud self-flags)windowvalue rejected by schema (unit-tested only, not live-verified)🤖 Generated with Claude Code
https://claude.ai/code/session_01RHo6AXm8i6s9Q4kJ78spHN