This Home Assistant integration allows you to monitor and control your APsystems EZHI inverter via the local API. It provides sensors for real-time data, alarm monitoring, and controls for power settings.
- Monitor PV Power & Energy: Track photovoltaic power generation and total energy production.
- Battery Monitoring: View battery state, charge/discharge rates, temperature, and status.
- Grid Interaction: Monitor power flow to and from the grid.
- Alarm Monitoring: Get notified about system errors and warnings via 20 binary sensors.
- Power Control: Set the maximum power output of your inverter.
- Separate Scan Intervals: Configure fast polling for power data and slower polling for alarms/device info.
- Device Info Panel: View firmware version, serial number, and direct link to inverter API.
- Multi-language Support: English and German translations included.
- Cloud Control (optional): On/off, system mode, backup power (EPS), ECO, SOC limits and more — none of which exist in the local API.
Before installing this integration, you need to:
- Ensure your APsystems EZHI inverter is connected to your local network
- Activate local mode on the inverter through the APsystems app. The vendor manual ties this to the one write: "This command only takes effect after enabling local mode in the APP" — and only to that. Reading was measured to work in every system mode, so an inverter left in another scenario still feeds every sensor here. What is untested is a device that has never been put into Local mode at all
- Set a static IP address for the inverter in your router (recommended)
- Add this repository as a custom repository in HACS
- Install the integration from HACS
- Restart Home Assistant
- Add the integration through the Home Assistant UI (Settings > Devices & Services)
- Download the latest release
- Extract the
apsystems_ezhi_localfolder to yourcustom_componentsdirectory - Restart Home Assistant
- Add the integration through the Home Assistant UI
- Go to Settings > Devices & Services
- Click "Add Integration" in the bottom right
- Search for "APsystems EZHI Local API" and click on it
- Enter the inverter's IP address and name
- Click "Submit"
After initial setup, you can change the scan intervals without reconfiguring:
- Go to Settings > Devices & Services
- Find "APsystems EZHI Local API" and click "Configure"
- Adjust the intervals:
- Power data interval: Fast updates for ogP, pvP, batP etc. (default: 5s)
- Alarms & device info interval: Slower updates for alarms and device info (default: 60s)
- Click "Submit" - the integration will reload automatically
| Entity | Description | Unit |
|---|---|---|
| Battery Status | Current battery state (Idle/Charging/Discharging/Fault/Shutdown/No Communication) | - |
| Photovoltaic Power | Current power generation from solar panels | W |
| Photovoltaic Energy | Total energy generation from solar panels | kWh |
| Battery Power | Current battery charge/discharge rate | W |
| Battery State of Charge | Current battery charge percentage | % |
| Battery State of Health | Battery health percentage | % |
| Battery Temperature | Battery temperature | °C |
| Battery Total Charge Energy | Total energy charged to the battery | kWh |
| Battery Total Discharge Energy | Total energy discharged from the battery | kWh |
| Battery Capacity | Battery capacity | kWh |
| On-Grid Power | Current power flow to/from the grid | W |
| On-Grid Output Energy | Total energy output to the grid | kWh |
| On-Grid Input Energy | Total energy input from the grid | kWh |
| Off-Grid Power | Current power flow to/from off-grid loads | W |
| Off-Grid Output Energy | Total energy output to off-grid loads | kWh |
| Off-Grid Input Energy | Total energy input from off-grid sources | kWh |
| Device Temperature | Inverter temperature | °C |
| Entity | Description | API Field |
|---|---|---|
| Battery Overtemperature | Battery high temperature protection active | BatHTP |
| Battery Undertemperature | Battery low temperature protection active | BatLTP |
| Battery Communication Error | Battery communication error detected | BatCE |
| Battery Overvoltage | Battery overvoltage protection active | BatHV |
| Battery Undervoltage | Battery undervoltage protection active | BatLV |
| Battery Overcurrent | Battery overcurrent protection active | BatHI |
| Battery Error | General battery error detected | BatE |
| Battery Shutdown | Battery shutdown state | SBS |
| Device Overtemperature | Device high temperature protection active | DTP |
| Device Error | General device error detected | EE |
| AC Abnormal | AC grid abnormality detected | ACA |
| Off-Grid Overcurrent | Off-grid overcurrent protection active | OfOI |
| Off-Grid Short Circuit | Off-grid short circuit protection active | OfGS |
| PV Overvoltage | PV overvoltage protection active | PvHV |
| PV Overcurrent | PV overcurrent protection active | PvOC |
| PV Wiring Error | PV wiring error detected | PVWE |
| IRD Error | IRD (Insulation Resistance Detection) error | IRDE |
| SOC Calibration Needed | Battery SOC reading is off — charge to 100% to recalibrate | BCC |
| Battery Access Conflict | A battery is connected while the inverter runs in battery-free mode | BCI |
| Voltage Reset Protection | PV input too low, or protection after a grid anomaly/overload — a restart is needed and can take several minutes | VRP |
The last three are reported by getAlarm on current firmware. On firmware that
does not send them they read unknown rather than "no problem".
Some alarms are transients, and the poll will miss them.
ACAwas measured lasting about two seconds after a grid outage — it marks the moment the grid goes away and clears again once the inverter has settled into island operation. The alarm endpoint is polled every 60 s by default, so a sensor here catches an event like that roughly one time in thirty. Do not build an outage detector onAC Abnormal; useOn-Grid Powerat zero together with a negativeBattery Power, which means the battery is carrying the off-grid load alone. Shortening the alarm interval helps a little and costs a request per second — it does not make a two-second event reliable.How much of this generalises to the other nineteen codes is untested.
ACAhangs off the grid monitor, which can only run while the inverter is grid-following, so it may well be the exception rather than the rule.
Each alarm sensor carries that text as attributes — cause and
suggested_action, plus the vendor's own vendor_name and the alarm_code —
so a sensor that goes to Problem also tells you what the app would have told
you. German if Home Assistant is set to German, otherwise English. They are
excluded from the recorder, being static.
docs/alarms.md has the same for all twenty in one place.
docs/alarms.json is the machine-readable copy, for anyone
reading getAlarm from Node-RED or a script instead of from this integration.
BCI and VRP were added to the vendor's Local API manual in V1.3 (2026-02-04).
BCC is in none of its versions, so do not expect to find it there: it is
undocumented but present in the getAlarm response (verified on firmware
1.9.0.16, 20 fields) and carried by the app, which builds its alarm screen from
whatever keys the response contains — for every field set to "1" it looks up
<FIELD>_name and <FIELD>_reason in its translations, and those exist for
BCC in all twelve shipped languages ("SOC Calibration" / "There is an error in
the battery SOC. Please charge the battery to 100%."). The integration maps it
for the same reason: the device sends the field, whether or not the manual
lists it.
- Max Output Power: the on-grid setpoint, -1200 W to +1200 W. Positive
discharges to the grid, negative charges from it — measured, and the
opposite of what this file and
services.yamlsaid before v0.5.2
This only does anything in the Local system mode. The vendor manual says so in one line under
setPower— "This command only takes effect after enabling local mode in the APP" — and it is easy to miss, so: measured across all four modes, with the setpoint written to -300 W, the inverter followed it in Local (grid flow went from -146 W to +272 W) and ignored it in Balcony Storage, Portable and AI, where it kept running its own strategy.setPoweranswersSUCCESSin every mode, so a write that changes nothing looks exactly like one that works — writing it outside Local logs a warning here for that reason. The app matches: it only sends a power target (userSetPower) in the Balcony Storage and Portable scenarios, and its Local mode screen offers no power control at all — that is the slot the local API writes into.
The local API is read-only apart from setPower. On/off, the system mode and
backup power (EPS) are not in it at all — they exist only in the APsystems
EMA cloud. This integration can talk to that cloud as a second, fully separate
layer.
The local side never depends on it. The cloud runs on its own coordinator:
dead credentials, an unreachable cloud or a hanging request take out the cloud
entities only. Every local sensor keeps updating, and Home Assistant offers a
reauth prompt instead of failing the whole entry. Verified against a live
install: with a deliberately broken token, all four cloud entities went
unavailable and all 130 local entities kept their values.
Leave the token fields empty and nothing about the integration changes.
Go to Settings → Devices & Services → APsystems EZHI → Configure and enter the username and password of your APsystems EMA account. The integration performs the same login the app does and stores the resulting token pair.
The account username, not the e-mail address you may also log in with —
loginEncryptrejects the address. Verified against a live account.
The password is used once and never stored — only the tokens are written to
the config entry, and the refresh_token does not rotate, so the login only has
to succeed once. The account fields stay empty afterwards for that reason; to
switch accounts, fill them in again.
There is no documented API for this. The login endpoint
(POST /api/token/generateToken/user/loginEncrypt) encrypts the credentials
client-side: a fresh AES-256 key and IV per login, both RSA-wrapped under a
public key baked into the app. That scheme is reproduced in cloud.py from the
app's own implementation, so no HTTPS proxy capture is needed. If you already
have a captured token pair, the two token fields still accept it directly.
| Entity | Type | Notes |
|---|---|---|
| Inverter On | switch |
One-way from HA. Once off, the inverter drops off the cloud's MQTT link and cannot be turned back on remotely — it needs PV/DC input or a 3 s press on the battery button. |
| System Mode | select |
Balcony Storage, Portable, AI, Local, No Battery. These are operating scenarios, not the Local API toggle: the local API answered in every one of them when tested, and a user on the vendor forum polls it while running Portable. ACA on the local API. It only stood for about two seconds, which is the likelier reason nobody sees these alarms. See the alarm note above. |
| Backup Power (EPS) | switch |
Mutually exclusive with ECO — enabling one clears the other in a single write. |
| ECO Mode | switch |
The opposite policy to EPS for the same output stage, which is why the firmware treats them as exclusive: EPS keeps the off-grid output armed, ECO drops it after an hour with no load. Recovery is via the AC output switch. An A/B here measured ~17 W of standby either way — but see below. |
| SOC Minimum / Maximum | number |
Percent. |
| Discharge Protection | number |
Refused below SOC minimum + 2 %, the same rule the app enforces. |
| Preset Output Power | number |
Watts. |
| Power Limit | sensor |
Read-only — see below. |
The output ceiling is 800 W by default and can be raised to 1200 W. The APsystems app puts a disclaimer in front of that: it "may cause the device output to exceed regulatory limits for grid connection", with the legal risk on the operator.
Home Assistant has no confirmation dialog for an entity — a switch is always one tap — so this is an action instead:
action: apsystems_ezhi_local.set_high_power_mode
data:
enable: true
acknowledge_regulatory_risk: true # required only when enablingLowering the ceiling is refused while the weekly output schedule still has entries above it. The vendor app silently rewrites those; this integration tells you which ones are in the way and leaves your schedule alone.
Two writes are refused rather than passed on:
- "No Battery" mode while a battery is connected — the "battery access conflict" the app warns about. The refusal lifts by itself once the cloud stops reporting a battery.
- Discharge protection below SOC minimum + 2 % — otherwise the device clamps it silently.
What ECO actually saves is unmeasured. Its documented job is shutting down the off-grid output stage when nothing has drawn from it for an hour — the opposite of what EPS does with the same stage, which is why the firmware treats the two as exclusive. An A/B on the development install showed ~17 W of total standby in both positions, but that does not settle it: a saving can only appear with the off-grid output up, nothing drawing from it, and the hour elapsed — and since toggling ECO moves EPS with it, the two positions are not otherwise identical. Measuring the off-grid stage on its own would settle it.
Two further device-side settings are readable in the cloud config but have no known write path, and both can make an entity above look more authoritative than it is:
winter— a field in the cloud config whose effect is unclear, and which nothing here writes. The app ships translations for a "Winter Adaptive Button" saying it would raise the SOC floor to 50 % and discharge protection to 65 %, but no screen in the app uses them: the key appears in the twelve language bundles and in no component, and the field itself is never read or written by the app. On the development installwinterreads "1" while the battery has gone down to 52 % — well below the 65 % that text describes. So the flag is either inert or unimplemented in this firmware. Documented so nobody re-derives it and, as I first did, mistakes a battery that simply stopped being discharged for a floor being enforced.- The weekly output schedule (
outputPowerStrategyWeekly) is read to guard the power limit, and never written.isOPStrategy: 1does not mean it is in effect — in Local mode it is inert: measured at 1125 W output inside a window the schedule caps at 50 W.
The integration creates 47 entities. examples/ has a dashboard that sorts
them into something usable — power right now, battery, controls, energy
totals, alarms, history:
| File | Needs |
|---|---|
examples/dashboard.yaml |
Mushroom and fold-entity-row from HACS |
examples/dashboard-core.yaml |
nothing — built only from cards Home Assistant ships with |
Same layout either way. Without the two custom cards installed, the first file renders "Custom element doesn't exist" where they would be, so take the second one if you would rather not install anything.
Settings → Dashboards → Add dashboard → New dashboard from scratch, then paste the file into the raw configuration editor (pencil → three dots → Raw configuration editor).
Both files assume the integration was added with the name ezhi. Replace
that throughout if you used something else. Upgrading from 0.4.0 or earlier?
Entity ids are handed out once, at first registration, so an existing install
keeps the ones it already has — and there the cloud entities and the local
power number carry an extra apsystems_ (switch.apsystems_ezhi_backup_power).
The exact ids for your install are on the device page.
The control section is marked for deletion if you do not use cloud control. It
is not hidden by a conditional card on purpose: the frontend's condition check
reads hass.states[entity]?.state, so for an entity that does not exist at all
a state_not: unavailable condition evaluates true — the card would appear
exactly when it should not.
The integration uses the following local API endpoints:
| Endpoint | Description |
|---|---|
/getDeviceInfo |
Device information (ID, type, battery info) |
/getOutputData |
Real-time power and energy data |
/getAlarm |
Alarm/error status |
/getPower |
Current power limit setting |
/setPower?p=XXX |
Set the on-grid setpoint. Positive discharges to the grid, negative charges from it. Local system mode only |
Bruno API collection files are included for testing.
Cloud endpoints live under https://app.api.apsystemsema.com:9223/aps-api-web/api/v2/.
The /api/v2 segment is not optional: without it every endpoint answers HTTP
200 with body code 4 "Internal Server Error", which looks like a cloud outage
rather than a wrong path.
- Cannot connect: Ensure the inverter is connected to your network. Note that the system mode is not the cause: the local API answered in all four modes when tested, so a mode other than Local does not explain missing sensor data
- The setpoint does nothing: check the system mode.
setPoweris accepted and answered withSUCCESSin every mode but only acted on in Local - During a grid outage the inverter keeps answering: it runs on the battery,
stays on Wi-Fi and serves all four endpoints,
getAlarmincluded. Measured across three outages with no dropped request. So sensors going unavailable is not what a power cut looks like — that is a network problem - Entities unavailable: Check if the inverter is powered on and operating
- Stale data: Try reducing the update interval in the integration options
- Fixed: the documented sign of the on-grid setpoint was inverted. Positive
discharges to the grid, negative charges from it — measured, and confirmed
against the device's own
ogP/batPsigns, which the vendor manual does define. The wrong version was inservices.yaml(so it showed in the service picker), in both example dashboards and in this file. Anyone who followed it charged when they meant to discharge. - New: writing the on-grid power setpoint while the inverter is in any mode
other than Local now logs a warning. Measured across all four selectable
modes: the device follows the setpoint only in Local and answers
SUCCESSeverywhere, so until now an ignored write was indistinguishable from a working one. The write still goes out — the mode reading can be a poll interval stale, and blocking on that would be worse than the silence it replaces. Needs cloud control configured; without it the mode is unknown and nothing is logged. - Corrected: leaving Local mode does not stop the local API. All four
endpoints answered
SUCCESSin every mode, so the sensors keep updating. What Local mode gates is the one write, not the reads. The old claim was in the README, the select entity's docstring and itslocal_mode_noteattribute. - Corrected:
BCCis undocumented rather than absent — the vendor manual lists 19 alarm fields as of V1.3 (2026-02-04), the device sends 20. The README now names the evidence instead of asserting the mapping.
Documentation only — no code changes, nothing to reconfigure.
- Corrected: the
winterflag does not raise the effective SOC floor. That claim came from correlating a battery that had not gone below 53 % with a flag set to 1. The daily minima actually scatter from 52 % to 83 %, which is a battery that stopped being discharged, not one hitting a floor — and the flag's own text promises discharge protection at 65 %, which the same install went below. The strings exist in all twelve language bundles but no screen uses them, and the app never reads or writes the field. - ECO now says what it is for: the opposite policy to EPS for the same output stage, which is why the firmware treats them as exclusive. The earlier claim that it does not reduce standby draw was more than one A/B supports, and is now stated as unresolved.
- Fixed: every entity set
has_entity_name, so the device name is no longer baked into the entity name as well — the frontend showed "EZHI APsystems EZHI Backup Power" where it puts device and entity side by side - New installs get uniform entity ids: the cloud entities and the local power
number lose their extra
apsystems_prefix (switch.ezhi_backup_power, notswitch.apsystems_ezhi_backup_power) - Existing installs are unaffected. Entity ids are assigned once, at first registration; a later name change only updates the registry's stored name. Automations, dashboards and history keep working — the example dashboards now match a fresh install, so check the device page for your own ids
- New: optional cloud control — on/off, system mode, backup power (EPS), ECO, SOC limits, discharge protection and preset output power, none of which exist in the local API
- New:
set_high_power_modeaction, gated behind an explicit acknowledgement of the vendor's regulatory disclaimer - Cloud runs on its own coordinator: a cloud failure cannot take the local sensors down, and dead credentials trigger a reauth prompt
- 88 unit tests for the cloud client, no network and no Home Assistant needed
- Cloud login with the EMA account username and password — no HTTPS proxy capture needed
- Example dashboard in
examples/, in a HACS and a built-in-cards variant - Both actions take an optional
device_id; with several inverters set up they refuse rather than silently pick one - Minimum Home Assistant version raised to 2024.11
- New: three alarm sensors the local
getAlarmendpoint reports but the integration ignored — SOC Calibration Needed (BCC), Battery Access Conflict (BCI) and Voltage Reset Protection (VRP) - On firmware that does not send them they read
unknownrather than "no problem", so an absent field is never reported as an all-clear - Manifest version corrected: it still said 0.2.0 while the changelog below claimed 0.2.1, so HACS never saw that release
- New: Added brand folder with icon.png and logo.png
- Support for Home Assistant 2026.3+
- New: Battery Status sensor - Shows Idle/Charging/Discharging/Fault/Shutdown/No Communication
- New: 17 binary alarm sensors - Monitor all inverter alarms and errors
- New: Separate scan intervals - Fast polling for power data (default: 5s), slow polling for alarms/device info (default: 60s)
- New: Device Info Panel - Shows firmware version, serial number, and configuration URL in HA device panel
- New: Options Flow - Change scan intervals after setup without reconfiguring
- New: German translations - Full German language support
- Fixed:
batS(Battery Status) was read from wrong JSON level in API response - Fixed: Device info now updates periodically (not just once at startup)
- Initial release with basic sensor and power control functionality
This project is released under the MIT License.
This integration is based on the APsystems EZ1 API Home Assistant integration by Sonnenladen GmbH.