Envoy lifetime battery energy CT counters intermittently report exactly 50% of value, corrupting long-term statistics
integration: enphase_envoy
### The problem
On a metered Envoy with IQ Batteries, the two storage-CT lifetime energy sensors —
`sensor.envoy_<serial>_lifetime_battery_energy_charged` and
`..._lifetime_battery_energy_discharged` — intermittently report **exactly 50% of their
true value** for a single poll, then recover on the next poll (~60s later).
Because both are `state_class: total_increasing`, the recorder treats each halving as a
meter reset and compensates. Each dip-and-recover cycle therefore injects roughly **one
full lifetime value** into the statistic's cumulative `sum`, permanently. Over ~9 days
this inflated the `charged` statistic's `sum` to **493 MWh** against a true lifetime value
of about **8.8 MWh** — a factor of ~56 — which makes the Energy dashboard's battery flow
wildly disproportionate to grid/solar in the Sankey diagram.
#### Evidence: the dips are exactly 0.5, never anything else
Analysing ~9 days of raw state history (the recorder's retention window) for both entities:
| | charged | discharged |
|---|---|---|
| raw states examined | 7693 | 1302 |
| decreases observed | 105 | 105 |
| decreases with ratio **exactly 0.500000** | **105 (100%)** | **105 (100%)** |
| `unavailable` transitions | 11 | 11 |
The two entities dip at **105/105 identical timestamps** — zero divergence — so this is a
single upstream event affecting the whole storage-CT payload, not per-sensor noise. Rate is
steady at ~10–14 events/day, not a one-off.
A representative dip, at full poll resolution:
```
2026-08-15T20:16:47 3.338746
2026-08-15T20:17:51 1.669373 <-- ratio to previous = 0.500000
2026-08-15T20:18:54 3.338747 <-- recovered, next poll
```
#### Evidence: the mechanism, checked arithmetically
Per dip-and-recover cycle, with true value `V`:
- dip `V → V/2` is a decrease, so reset-compensation adds `V/2` to `sum`
- recovery `V/2 → V` is a normal increase, so it adds another `V/2`
- net injected per cycle: **`V`**
Predicted vs observed inflation over the same 9-day window:
```
charged: 105 cycles x 3.335 MWh = 350 MWh predicted
observed sum growth 2026-08-08 -> 2026-08-17 = 380 MWh
```
The ~8% residual is consistent with the 11 `unavailable` transitions (which also trigger
compensation) plus dips falling in boundary hours. The mechanism accounts for the runaway.
Reconstructing `sum` from the entity's own `state` history using
`sum[i] = sum[i-1] + max(0, state[i] - state[i-1])` over the sensor's full ~14-month
lifetime yields **8.77 MWh charged / 5.32 MWh discharged**, versus **493 / 296 MWh** stored.
#### Where the value comes from
Tracing it in the installed code:
- `sensor.py` `CT_SENSORS`: `(CtType.STORAGE, "lifetime_battery_charged")` uses
`value_fn=attrgetter("energy_received")`, and `"lifetime_battery_discharged"` uses
`attrgetter("energy_delivered")`.
- `pyenphase/models/meters.py`: `energy_delivered=round(data["actEnergyDlvd"])`,
`energy_received=round(data["actEnergyRcvd"])`.
There is no arithmetic on these fields in pyenphase — they are passed through from the
device payload. So the halving appears to originate in what the Envoy itself reports for the
storage CT, rather than in the library's parsing.
**Relevant context:** Enphase support has confirmed that this system is currently in a
**battery calibration state set by their service technicians**. That is the most likely
trigger for the device emitting these half-value readings, though I cannot verify what the
Envoy is doing internally.
#### Why I don't think the existing documented issues cover this
The integration docs describe Envoy firmware issues where *Lifetime energy production*
resets to 0, or decreases by a **fixed 1.2 MWh**. Both are additive/absolute anomalies on
the production CT. What is happening here is a **proportional ×0.5** anomaly on the
**storage** CT, which is a different signature, and the existing "correct Envoy lifetime
production energy" blueprint does not apply to the battery entities.
#### What I'd like to ask for
Granting that the bad value likely originates in device firmware: the integration currently
converts a **transient, self-correcting device glitch into permanent, irreversible
corruption** of long-term statistics, because `total_increasing` compensation runs on a
value that is never physically valid. A lifetime CT counter halving is not a real meter
reset.
Would the maintainers consider a sanity guard on the lifetime CT counters — e.g. treating an
implausible proportional drop in a lifetime counter as a bad read (skip the update / mark
unavailable) rather than publishing it? There is precedent for the integration defending
against bad device data during dropouts (#120091, addressed via the pyenphase bump in
#121583). Without something like this, every affected user has to manually repair statistics
repeatedly, and the corruption is silent until the Energy dashboard visibly breaks.
Two things make that worse in practice than it might sound:
**The corruption is still accumulating.** These are not historical events — the halvings are
ongoing at ~10–14 per day as of filing, so the statistic degrades continuously for as long as
the device stays in this state. Any repair a user performs is undone within hours.
**Repairing it is genuinely hard to get right.** Statistics live in two tables, and the
obvious repair path only fixes one of them. `recorder/import_statistics` writes the hourly
long-term `statistics` table, but the recorder compiles each new hourly row *from* the
5-minute `statistics_short_term` table. So a long-term-only repair looks completely correct —
until the next hourly compile silently reverts it from the still-inflated short-term baseline.
Restarting Core does not help, because the stale baseline is in the database rather than in
memory. `recorder/adjust_sum_statistics` is what reaches short-term. This is not obvious from
the UI's "Adjust sum" affordance, and it means a user who follows the documented statistics
repair route can reasonably believe they have fixed it when they have not.
#### Workaround, for anyone else hitting this
Since a lifetime counter can never legitimately decrease, a running max fully rejects the
glitch without needing to special-case the 0.5 ratio. Point the Energy dashboard's battery
configuration at filtered template sensors rather than the raw entities:
```yaml
template:
- sensor:
- name: Envoy Lifetime Battery Energy Charged Filtered
unique_id: envoy_lifetime_battery_energy_charged_filtered
unit_of_measurement: MWh
device_class: energy
state_class: total_increasing
availability: >
{{ states('sensor.<serial>_lifetime_battery_energy_charged')
not in ['unknown', 'unavailable', 'none', ''] }}
state: >
{% set raw = states('sensor.<serial>_lifetime_battery_energy_charged') | float(0) %}
{% set prev = this.state | float(0) %}
{{ raw if raw >= prev else prev }}
```
(Repeat for `..._discharged`.) The deliberate trade-off is that these freeze at the last good
value if the Envoy is ever genuinely replaced or reset, and need clearing by hand.
This is a user-side band-aid, not a substitute for a guard in the integration — it only helps
people who already know their statistics are being corrupted, which is precisely the thing
that is invisible until the damage is done.
#### An open question I could not resolve
I cannot determine from Home Assistant's data alone **which value is actually correct**. If
the aggregate normally sums a per-phase field that already represents the whole system, the
"dip" to half could be the *correct* reading and the usual value a double-count. Comparing
against the Enphase Enlighten app would settle it, and it would be useful to know whether
this aggregate is expected to be phase-summed on a split-phase system.
### What version of Home Assistant Core has the issue?
core-2026.8.2
### What was the last working version of Home Assistant Core?
Unknown — I do not believe this is a Core regression. The statistic's `sum` was still sane
(~3.6 MWh, tracking `state`) as of 2026-05-31 and diverged sharply from early August 2026,
which correlates with the battery entering the service-technician calibration state rather
than with any Core upgrade.
### What type of installation are you running?
Home Assistant OS
### Integration causing the issue
enphase_envoy
### Link to integration documentation on our website
https://www.home-assistant.io/integrations/enphase_envoy/
### Diagnostics information
Can provide a config entry diagnostics dump and/or the full hourly statistics export for both
entities on request — omitted here as it contains the Envoy serial and site details.
### Anything in the logs that might be useful for us?
```txt
2026-08-15 23:07:47.897 ERROR (MainThread) [homeassistant.components.enphase_envoy.coordinator] Error fetching Envoy <serial> data: Error communicating with Envoy API on <ip>: aiohttp ClientError Timeout on reading data from socket
2026-08-16 15:34:37.727 ERROR (MainThread) [homeassistant.components.enphase_envoy.coordinator] Error fetching Envoy <serial> data: Error communicating with Envoy API on <ip>: aiohttp ClientError Timeout on reading data from socket
2026-08-16 16:50:46.353 ERROR (MainThread) [homeassistant.components.enphase_envoy] Setup of config entry 'Envoy <serial>' for enphase_envoy integration cancelled
2026-08-16 23:11:32.426 ERROR (MainThread) [homeassistant.components.enphase_envoy.coordinator] Error fetching Envoy <serial> data: Error communicating with Envoy API on <ip>: aiohttp ClientError Timeout on reading data from socket
2026-08-17 02:37:57.003 INFO (MainThread) [homeassistant.config_entries] Config entry 'Envoy <serial>' for enphase_envoy integration not ready yet: Error communicating with Envoy API on <ip>: 500; Retrying in 5 seconds
```
These comm errors are far less frequent (a handful over the same period) than the 105 halving
events, so they do not appear to be the direct trigger — the halvings mostly occur on polls
that otherwise succeed.
### Additional information
- Hardware: Envoy metered (split-phase), 8x IQ Battery / Encharge units, 40 kWh usable.
- `pyenphase==3.2.1` (current latest on PyPI at time of filing).
- The per-phase entities (`..._l1` / `..._l2`) are disabled on this install, so I have not
been able to check whether the individual phase counters halve at the same moments. Happy
to enable them and capture that if it would help isolate this.
- Still occurring at time of filing; I can capture fresh samples, a diagnostics dump, or the
full hourly/5-minute statistics export on request while the system remains in this state.
If it would help, I can also confirm whether the halvings stop once Enphase takes the
battery out of calibration, which would pin the trigger.
关闭于 5 天前 3 条评论