Skip to content

Commit ffe1c8d

Browse files
committed
Fix consumption-entity resolution: registry lookup instead of guessed entity_id
Verifying the hourly-profile data path surfaced the reason the profile-aware overnight reserve might not take effect: _resolve_consumption_entity GUESSED the inverter load sensor's entity_id as `sensor.{title}_{key}`. But HA derives the entity_id from the slugified friendly NAME, e.g. "Homeload Day Cost Energy (0.1KWh)" → `sensor.<title>_homeload_day_cost_energy_0_1kwh` — so the guess never matched, hass.states.get() returned None, resolution failed, and the hourly profile silently fell back to a FLAT daily/24 distribution. A flat profile makes the profile-aware reserve identical to the old flat reserve, so the daytime-EV phantom-deficit fix would have had no effect on real installs. Now resolves the entity via the entity registry using the exact unique_id the integration assigned (`{entry_id}_{key}`) — exact and rename-proof. Keeps the override entity as first choice and the old title-guess as a last-resort fallback. Rest of the path verified sound: recorder statistics_during_period for hourly buckets, 7-day averaging, persisted + recomputed on startup (no cold-start gap), exposed as consumption_hourly_profile for inspection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P9LAQ5ET6SU9dLWULxv2uF
1 parent 284b7fa commit ffe1c8d

2 files changed

Lines changed: 58 additions & 3 deletions

File tree

CLAUDE.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1313,7 +1313,34 @@ extrapolation from today's actual production) so the algorithm doesn't
13131313
default to PV=0 and over-aggressively grid-charge.
13141314

13151315
#### C. Consumption Profile Awareness — IMPLEMENTED
1316-
Hourly consumption profiles from 7-day HA recorder history. `EMSState.consumption_hourly_kwh` provides per-hour averages used in `_project_soc_trajectory()` and `_validate_schedule_soc()`. Coordinator records hourly breakdown at midnight. Frontend card also uses profiles.
1316+
Hourly consumption profiles from 7-day HA recorder history. `EMSState.consumption_hourly_kwh` provides per-hour averages used in `_project_soc_trajectory()`, `_validate_schedule_soc()`, and (July 2026) the overnight reserve. Coordinator records hourly breakdown at midnight. Frontend card also uses profiles.
1317+
1318+
**Data path** (`coordinator`):
1319+
1. `_resolve_consumption_entity` — picks the source: the user's
1320+
`consumption_override_entity` if set, else an inverter load-energy sensor
1321+
(`homeload_day_cost_energy` / `load_day_cost_energy` / …) resolved via the
1322+
**entity registry** by the exact `unique_id` (`{entry_id}_{key}`).
1323+
2. `_query_hourly_from_history` — HA recorder `statistics_during_period`
1324+
("change" per hour) → 24 hourly buckets. Falls back to a FLAT daily/24
1325+
distribution when no statistics exist.
1326+
3. `_record_hourly_consumption` (midnight) appends to a 7-day
1327+
`_hourly_consumption_history`; `_calculate_hourly_profile` averages it into
1328+
`_hourly_consumption_profile`. Persisted in the consumption Store and
1329+
**recomputed on startup** from the loaded history (no cold-start gap).
1330+
4. Exposed as `consumption_hourly_profile` in `schedule_status` attributes —
1331+
inspect it to confirm the shape (low-night/high-day for EV loads). A FLAT
1332+
profile (all hours equal) means the history query fell back (no stats or,
1333+
pre-fix, an unresolved entity).
1334+
1335+
**Registry-lookup fix (July 2026)**: `_resolve_consumption_entity` previously
1336+
GUESSED the entity_id as `sensor.{title}_{key}`. HA derives the entity_id
1337+
from the slugified friendly NAME (e.g. "Homeload Day Cost Energy (0.1KWh)" →
1338+
`..._homeload_day_cost_energy_0_1kwh`), so the guess never matched → the hourly
1339+
profile silently fell back to FLAT → the profile-aware overnight reserve had no
1340+
effect (daytime-heavy / EV loads looked flat overnight again). Now resolved
1341+
via `entity_registry.async_get_entity_id("sensor", DOMAIN, unique_id)` — exact
1342+
and rename-proof. Best accuracy still comes from setting a
1343+
`consumption_override_entity` (a real P1/energy meter).
13171344

13181345
#### C2. SOC History Display — IMPLEMENTED
13191346
Coordinator records battery SOC at each slot boundary in `_soc_history`. Frontend draws solid line for actual past SOC, dotted line for projected future.

custom_components/ha_felicity/coordinator.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,15 +1734,43 @@ async def _record_hourly_consumption(self, date_str: str) -> None:
17341734
self._calculate_hourly_profile()
17351735

17361736
def _resolve_consumption_entity(self) -> str | None:
1737-
"""Return the best entity_id to query for hourly consumption history."""
1737+
"""Return the best entity_id to query for hourly consumption history.
1738+
1739+
Resolution order:
1740+
1. The user's consumption_override_entity (a real energy meter / P1).
1741+
2. An inverter load-energy sensor this integration created — looked
1742+
up via the ENTITY REGISTRY by the exact unique_id the sensor was
1743+
registered with (`{entry_id}_{key}`). The old code GUESSED the
1744+
entity_id as `sensor.{title}_{key}`, but HA derives the entity_id
1745+
from the slugified friendly NAME (e.g. "Homeload Day Cost Energy
1746+
(0.1KWh)" → `..._homeload_day_cost_energy_0_1kwh`), so the guess
1747+
never matched → resolution failed → the hourly profile silently
1748+
fell back to a FLAT distribution. That flat profile defeats the
1749+
profile-aware overnight reserve (daytime-heavy / EV loads look
1750+
like flat overnight consumption again). The registry lookup is
1751+
exact and rename-proof.
1752+
"""
17381753
if self.consumption_override_entity:
17391754
return self.consumption_override_entity
1740-
# Try to find the sensor entity created by this integration for load energy
1755+
1756+
from homeassistant.helpers import entity_registry as er
1757+
ent_reg = er.async_get(self.hass)
1758+
entry_id = self.config_entry.entry_id
17411759
for key in ["daily_energy_consumed", "daily_load_energy",
17421760
"total_load_consumption_energy_day",
17431761
"load_consumption_energy_day",
17441762
"homeload_day_cost_energy",
17451763
"load_day_cost_energy"]:
1764+
eid = ent_reg.async_get_entity_id("sensor", DOMAIN, f"{entry_id}_{key}")
1765+
if not eid:
1766+
continue
1767+
state = self.hass.states.get(eid)
1768+
if state and state.state not in ("unknown", "unavailable"):
1769+
return eid
1770+
1771+
# Legacy fallback: the old title-based guess (kept in case a future
1772+
# entity isn't in the registry under the expected unique_id).
1773+
for key in ["homeload_day_cost_energy", "load_day_cost_energy"]:
17461774
eid = f"sensor.{self.config_entry.title.lower().replace(' ', '_')}_{key}"
17471775
state = self.hass.states.get(eid)
17481776
if state and state.state not in ("unknown", "unavailable"):

0 commit comments

Comments
 (0)