Home Assistant integration for the Hager flow battery storage system with PV (E3/DC hardware underneath). Provides live power readings, battery state of charge and cumulative energy counters — the latter ready to use in the energy dashboard.
Two ways in. Preferred is Hager's official API at developer.hagerenergy.com: documented, and its credentials do not expire. The undocumented API behind the flow portal stays available for anyone without API access, at the price of a token that has to be replaced roughly every 30 days. Both produce the same set of entities.
| Entity | Unit | Notes |
|---|---|---|
| Battery level | % | |
| Solar power | W | |
| House consumption | W | derived by the device, includes the inverter losses |
| Battery power | W | positive = charging, negative = discharging |
| Grid power | W | positive = import, negative = export |
| Inverter power | W | disabled by default |
| Solar energy | kWh | cumulative, total_increasing |
| House energy | kWh | cumulative |
| Grid import / export energy | kWh | cumulative |
| Battery charge / discharge energy | kWh | cumulative |
| Online | — | connection to the cloud |
| Solar forecast today / tomorrow | kWh | expected production, official API only |
Four of those differ depending on which backend is in use:
| Entity | official API | flow portal |
|---|---|---|
| Solar power | installation total | sum of the three string registers |
| Solar energy | gross DC yield, reported as such | the AC balance, derived — a few percent lower, and here is why |
| Inverter power | not registered | sum of the AC phase registers |
| Solar forecast today / tomorrow | expected production in kWh | not registered |
Both report solar power on the DC side, before the inverter; only the counter behind Solar energy differs.
The last two are not merely unavailable on the route that lacks them — they are not created at all. Each backend declares what it can supply and the platforms register only that, because an entity that could never hold a value is worse than an absent one. Inverter power does exist on the official API's per-device measurements, which are a separate request this integration does not make.
There are also unsigned power variants (charge power, discharge power, grid import, grid export), disabled by default — handy for automations without templates.
The energy counters can be used directly in the energy dashboard:
- Grid → Grid import energy / Grid export energy
- Solar → Solar energy
- Battery → Battery charge energy / Battery discharge energy
The button adds this repository to HACS on the instance you are signed in to. By hand:
- HACS → ⋮ → Custom repositories
- Add
https://github.com/treee111/hagerFlow, category Integration - Install "Hager flow", then restart Home Assistant
Copy custom_components/hager_flow/ into <config>/custom_components/ and restart
Home Assistant.
Or by hand: Settings → Devices & Services → Add Integration → Hager flow. Either way, pick one of the two routes.
Request access at developer.hagerenergy.com. Self-service through the MyE3/DC portal is announced; until then Hager creates the OAuth client on request. You receive a client ID and a client secret — enter both, and the integration finds your installation itself. If the credentials cover more than one, it asks which.
Client ID and secret do not expire. The integration derives a five-minute access token from them and renews it on its own, so nothing has to be replaced by hand.
Needs no API access. Two values:
Serial number — shown in the portal title bar and in its URL, a twelve-digit number.
Refresh token — how to get it:
- Open flow.hager.com and sign in
- Open the developer tools (F12)
- Application → Local Storage →
flow.hager.com - Copy the value of
reAuthToken(without the surrounding quotes)
The token is valid for roughly 30 days. When it expires, Home Assistant reports a repair issue and asks for a new one through the regular reauth dialog — there is no need to set the integration up again.
Nothing stops you from adding the same installation twice, once per route, but the two entries identify it differently — by serial number on the portal, by installation id on the official API. Home Assistant therefore sees two devices with two full sets of entities rather than recognising them as one. The entry title names the route, so the two stay tellable apart.
Side by side the two agree on everything except the solar counter, which is the whole point: same live power to the watt, same grid, house and battery counters to the decimal, and a solar counter that sits a few percent higher on the official route because it is the gross DC yield.
There is no automatic migration between the routes. To move an existing setup over without losing its history:
- Add the official route and let it run alongside for a while, comparing the two
- Delete the old entry, which removes its entities but leaves their statistics
- Rename the new entities from
..._2back to the old entity ids, so they pick up those statistics again
Step 3 logs an error per entity, which is expected and harmless:
Cannot rename statistic_id `sensor.…_2` to `sensor.…`
because the new statistic_id is already in use
The entity is renamed all the same, and from then on it writes into the existing
series — which is exactly the point of renaming it. Only the short ..._2 series
recorded while both routes ran are left behind; clear those under Developer tools →
Statistics.
Step 3 then needs one correction, and only for the solar counter. Going from the AC
balance to the gross DC yield is a jump upwards of a few percent of everything
produced so far, and a total_increasing sensor reads a jump upwards as production —
so the energy dashboard would show one enormous solar hour. Wait until the hour
containing the jump has been compiled, which happens on the next full hour; before
that there is nothing to adjust and the attempt silently does nothing. Then use
Adjust sum on that hour, subtracting the difference between the two counters as
read at the same moment. What remains is the production that really happened in that
hour, so the figure is measured rather than estimated.
Purging the counter's statistics instead also works, at the price of its whole history — which is a poor trade, because the old values are not wrong. They are the AC yield, a different and equally valid quantity, so what the switch leaves behind is a change of definition on one date rather than an error to be erased.
The other five counters need no such care: they carry the same values on both routes, usually to the last decimal.
Both backends normalise their own registers into one shared vocabulary, so the
coordinator and the entities never see which one is in use. backend.py states that
contract; api.py and api_official.py implement it.
Live values are polled every 30 s and the cumulative counters every 5 minutes. The slower cadence for the counters is not a compromise: on both backends they only advance on a 15-minute grid and lag real time by about 17 minutes anyway, so polling them more often would just repeat the same numbers.
An OAuth 2 client credentials grant against Hager Energy's identity provider, then plain REST:
POST /realms/customer/protocol/openid-connect/token grant_type=client_credentials
GET /v1/installations discover the installation id
GET /v1/installations/{id}/energy/current live flows in W
GET /v1/installations/{id}/energy/total cumulative counters in Wh
The access token lives five minutes and the flow has no refresh token, so an expired
one is simply requested again. Responses are JSend-wrapped — the payload sits in
data.
The mapping is one to one. Nothing has to be derived, and no register name misleads:
pvProduction, gridFeedIn, gridConsumption, consumption, batteryCharge and
batteryDischarge all mean what they say. pvProduction in particular is the gross
DC yield, which the portal backend cannot report at all — there it has to be derived,
and what comes out is the AC balance, a few percent lower.
There is no online flag on energy/current; the reading carries a timestamp that
advances every 10 to 30 seconds, so a stale one is what marks the system offline.
GET /v1/installations/{id}/forecast/production/{YYYY-MM-DD}
Served for today and tomorrow only — any other date is refused with "must be
either today or tomorrow". Hourly watt-hours, which the integration sums into the
sensor value and carries along as an hourly attribute, so the profile can be
charted without a second request. Polled hourly, and a failure is swallowed rather
than raised: losing the forecast must not take the live readings down with it.
The sensors deliberately have no state class. A forecast is not a measurement, and letting it into long-term statistics would file predicted kilowatt-hours next to metered ones.
There is a consumption forecast at forecast/consumption as well, but it answered
with an empty values list on the installation this was built against, so it is not
wired up. An empty day is treated as no reading rather than as a confident zero.
The flow portal is a frontend for the E3/DC cloud. The integration signs in with the long-lived refresh token, derives a short-lived access token (10 minutes) from it and renews that on its own:
POST /auth-saml/re-auth {"reAuthToken": "..."} -> {"token": "..."}
GET /storages/{SN}/status Authorization: Bearer <token>
GET /storages/{SN}/history-values/difference cumulative counters in Wh
Battery, grid and consumption are reported per phase and PV per string; the integration sums each group.
The sign convention was verified against live readings in both directions — once while importing and charging, once while exporting — and the power balance adds up to the watt in both cases: solar plus battery discharge plus grid import equals house consumption plus battery charging plus grid export. Inverting either sign throws the balance off by hundreds of watts, so the mapping is unambiguous.
The cumulative counters need two corrections before they mean what their names suggest, both established by integrating the live power over several hours and comparing the result against the counter differences:
| Backend register | What it actually is |
|---|---|
NetIn |
grid export — the name is written from the grid's point of view |
NetOut |
grid import |
Production |
the balance without the battery, not the PV production |
Consumption, BatPowerIn, BatPowerOut |
as expected |
Production satisfies Consumption + NetIn - NetOut to the watt-hour at every
reading, which is why it keeps climbing overnight at roughly the house base load.
The integration therefore derives the PV counter as
Production + BatPowerIn - BatPowerOut.
That derivation makes the PV counter the balancing term, so the counters adding up
is true by construction and confirms nothing on its own. What pins the mapping down
is the comparison against the live power integrated over whole days: grid export,
grid import and both battery counters agree with it to well under one percent, and
swapping NetIn and NetOut puts grid import and export off by an order of
magnitude on a sunny day.
The PV counter is the one place where that comparison does not land on the nose.
Integrated over a full day, the live POWER_PV_S* sum comes out a few percent
above the counter — consistently, and in the same proportion on days with very
different yields. That is not an error in the derivation; the two simply live on
opposite sides of the inverter:
POWER_PV_S1..S3are DC string powers.- Every cumulative counter, and therefore the derived PV counter as well, is AC.
The cross-check: the AC energy implied by the counters (Consumption - NetOut + NetIn) matches the integrated POWER_AC register to about a percent, and dividing
it by the DC energy that passed through the inverter gives a plausible conversion
efficiency. The gap between the DC and the AC view is exactly that conversion loss.
House consumption inherits the same offset, because the device does not meter it —
it derives it as the residual of the live registers. That is why the live power
balance closes to the watt while POWER_C_L* integrated over a day exceeds the
Consumption counter by the same absolute amount that PV does. The live house
figure carries the inverter losses; the counter does not.
For the energy dashboard the AC basis is the right one here, since every other counter this backend reports is AC too. Just do not expect Solar power integrated over a day to equal Solar energy — the difference is the inverter, not a bug.
None of this applies to the official API, which reports the gross DC yield as
pvProduction and needs no derivation at all.
- Cloud dependent. No data without internet access or during a cloud outage. Locally the device offers neither a settable RSCP key (there is no configuration web interface) nor Modbus TCP; Hager support can enable either one.
- The counters lag real time by about 17 minutes and only advance on a 15-minute grid, on both routes, so a short window compared against live power will not balance. Over a few hours it does.
- Read only. This integration only reads. The official API does expose write endpoints — the grid feed-in limit, and wallbox controls once e-mobility goes live — but none of them are wired up here.
Only on the flow portal route:
- The refresh token expires after ~30 days and cannot be renewed automatically, because the refresh endpoint does not return a new one. The official API has no such limit.
- The counter register names cannot be taken at face value — see the table above. Upgrading from a version before this correction swaps grid import and export and raises the PV counter by the battery charge, so the long-term statistics of those three sensors are worth clearing once (Developer tools → Statistics); otherwise the old, wrong values stay in the energy dashboard.
- Power and energy are measured on different sides of the inverter. Solar power is DC, the counters are AC, and house power is a residual that includes the conversion losses — see above. Integrating a power entity will therefore not reproduce the matching counter, and the solar counter is the AC balance rather than the gross yield. This backend has no register for the DC yield; the official API reports it directly.
- Do not derive counters from the
fromfield ofhistory-values/difference. Its snapshots are unreliable — solar production appears to rise overnight. Only thetofield is used, which is the current reading; Home Assistant computes the differences itself. - Undocumented API that may change at any time.
Not read by this integration yet: hourly, daily, weekly, monthly and yearly energy history, per-device measurements including the inverter's AC output and per-string DC input, and the PV configuration (installed capacity, feed-in limit). Wallbox endpoints are announced but not live.
The per-device measurements are the interesting gap: they carry the AC output that would fill Inverter power on this route, and the per-string DC input the portal reports directly.
python3 tests/test_parse.py
python3 tests/test_translations.pyBoth run without credentials and without Home Assistant installed. The CI additionally runs hassfest and the HACS validation on every push.
MIT