~/blog/home-energy/solar-phantom-readings-firmware.md
A solar gateway firmware update booked 11 MWh of phantom solar
// tl;dr
An Envoy firmware update froze production and rebased the lifetime register, and Home Assistant booked about 11,562 kWh of fake solar in one hour. The CT counter never moved, so I reloaded the integration and rebuilt the statistics from it. An alert now watches the two registers.
My Enphase Envoy updated its own firmware one morning in late September 2026, from 8.3.5289 to 8.3.6086. I found out because Home Assistant showed 2.8 kW of solar at night, and the Energy tab said the house had used an absurd amount of power that day.
Nothing in the house had changed. The array was fine too, and one comparison proved it.
It was also the second time this year the same counter had gone bad. The first was in May.
Two faults from one update
The production endpoint froze. Current production sat at exactly 2.827 kW for almost 23 hours, and production today read 0.0.
The lifetime production register also rebased downward, from about 25.4 MWh to about 11.6 MWh. That sensor has a state class of total_increasing, and Home Assistant reads a drop in one of those as a meter reset. So it counted the new value as fresh energy and booked roughly 11,562 kWh of solar into a single hour.
Why it looked like a consumption problem
My Energy dashboard has no home consumption source configured. Home Assistant works usage out from the other flows:
usage = solar + grid import - grid export + battery out - battery in
Any phantom in solar lands in the usage bar one for one. The consumption side looked broken, but every bit of the error came from production.
The register that didn't move
The Envoy reports production twice. There's the lifetime register, and there's a separate energy delivered counter from the production CT. The CT counter never rebased. It read 51.9 kWh on the day of the update and 52.2 the next. Two ordinary days.
Two independent registers measure the same array, and only one jumped. That told me the panels were fine and the gateway's bookkeeping wasn't. It also gave me a clean reference to rebuild the statistics from.
My Envoy has no consumption CT, so I can't cross check the consumption side the same way.
The repair, in order
Reloading the integration's config entry unfroze the endpoint. The lifetime register came back at 25.53 MWh, matching the CT exactly.
Then the statistics. In the statistics tool under Developer tools, I adjusted the sum on the phantom hour by minus 11,559.670 kWh. I backfilled the 23 frozen hours from the CT and checked that against the raw register movement. They agreed to 0.005 kWh.
The reload made a second phantom. Climbing from 11.6 back to 25.5 MWh looked like new production, about 13,970 kWh in the reload hour. You can't adjust that hour until its statistics row exists, which on my install was two to five minutes past the hour. So there's a short wait.
To check the repair, I compared daily solar against the CT for every day in the window. The worst day was off by 0.01 kWh. Derived usage sat 1.3 to 2.8 kWh below the Envoy's own consumption meter, but that gap shows up on clean days too. It's battery round trip loss.
A guard for next time
Firmware shows on the Envoy's device page, but there's no entity for it, and the update itself was one warning line in the log. Nothing to trigger on directly. I watch the gap between the two registers instead. Both report MWh on my Envoy, hence the times 1000:
template:
- sensor:
- name: "Solar production register divergence"
unit_of_measurement: "kWh"
state_class: measurement
state: >
{{ ((states('sensor.solar_lifetime_production') | float(0))
- (states('sensor.solar_ct_delivered') | float(0))) | abs * 1000 }}
The measured noise floor was 0.012 kWh over 119 clean hours. A 1 kWh threshold for 10 minutes sits far above that and far below a fault like this one.
Divergence alone has a hole. If both registers freeze together, the gap stays at zero. So the automation has a second trigger for the lifetime register going stale:
triggers:
- trigger: numeric_state
entity_id: sensor.solar_production_register_divergence
above: 1
for: "00:10:00"
- trigger: template
value_template: >
{{ is_state('sun.sun', 'above_horizon')
and (now() - states.sun.sun.last_changed).total_seconds() > 10800
and (now() - states.sensor.solar_lifetime_production.last_updated).total_seconds() > 7200 }}
actions:
- action: notify.my_phone
data:
message: "Solar registers disagree or stopped updating (or the array is snow covered)."
The sun checks matter. Home Assistant only moves last_updated when the state changes, so at night every Envoy sensor looks stale.
My first version only checked above_horizon. It fired falsely about ten minutes after sunrise on 8 of 10 mornings. The two hour clock had started at the previous evening's last reading, so the register was already stale when the sun came up, and my array doesn't produce until well after the flat horizon sunrise. Requiring the sun to be up for three hours fixed it. A restart resets last_changed on sun.sun, which only delays the alert.
What I know and what I don't
Measured: the firmware change in the log, the frozen endpoint, the rebase, the CT staying put, and the repaired totals matching the CT day by day.
Not known: why the firmware rebased that register. That happened on the Enphase side, and I can't see into it.
What I'd tell myself
Before touching a single statistic, compare the two production registers. The one that didn't move is both the proof and the repair source.
$ ./ask --help
Stuck on something similar? Ask below and I'll try to help you work through it. It helps to include:
- --running hardware, OS, versions
- --tried what you've already tried
- --error the exact message, word for word
Comments are public, so leave out passwords, keys and public IP addresses.