A Home Assistant integration for local TCP control of AECC-platform home batteries. No cloud, no latency, no external dependencies.
Works with any battery built on the AECC platform: Lunergy, Sunpura, Voltdeer, AEG Solarcube, AFERIY, AccuMate, and others.
| Setup | Sensors | Controls | Energy Dashboard |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
- 100% local: communicates directly with your battery over TCP (port 8080)
- 5-second polling: near real-time updates with intelligent failure tolerance
- Physics-aware sensor cleaning: rejects sensor glitches (
SOC=0during active discharge, impossible rate-of-change spikes) before they reach Home Assistant or pollute the Energy Dashboard. Tuned per brand. - Hybrid availability: entities hold their last known value through brief sensor blips, then transition to
unavailableafter a sustained outage so automations and dashboards see an honest signal. - Write-back verification: every control command is re-read after writing; mismatches are logged so silent firmware drops become visible.
- Energy Dashboard ready: accumulated kWh sensors (
total_increasing) for the HA Energy Dashboard - Full battery control: direction (Charge/Discharge/Idle), signed power setpoint, per-direction power limits (up to 2400W), SOC limits
- Work mode selector: Self-Consumption (AI), Custom/Manual
- Multi-brand: select your brand during setup; DeviceInfo shows correct manufacturer and model
- Multi-unit: master/slave stacks get one device per battery with per-unit telemetry, plus whole-system totals
- Multi-language: English, Dutch, German, French
This integration works with batteries built on the AECC platform (ai-ec.cloud). The AECC platform is white-labeled by multiple battery brands that share the same local TCP protocol.
If your battery uses the AECC app (or a white-labeled version), connects to an ai-ec.cloud server, and has TCP port 8080 open on your local network, this integration should work.
| Brand | Model | Status | Notes |
|---|---|---|---|
| Sunpura | S2400 | Fully tested | PV input and multi-battery setups confirmed working |
| Lunergy | Hub 2400 AC | Fully tested | TCP connection can be flaky; the integration handles reconnects automatically. Per-brand sensor cleaning rejects the known sensor-stuck-at-zero pattern. |
| AEG | Solarcube | Partial | Monitoring works (#1). Battery power control has no effect on this firmware (it ignores the control register and uses dedicated power registers instead); needs a register scan from an AEG owner to fix (#8) |
| Voltdeer | SR5000 | Community confirmed | Works out of the box |
| AFERIY | PS240 | Community confirmed | Confirmed working (#2) |
| AccuMate | Plug-In Battery | Community confirmed | Works out of the box (#6) |
| JET | GreenARK Pro | Tested | Confirmed working on a loan test unit |
| Brand | Model | Notes |
|---|---|---|
| Other AECC brands | : | Any battery using the AECC / ai-ec.cloud platform may work |
Have a different AECC brand? We'd love to hear from you. Install the integration, try it out, and open an issue to let us know if it works (or doesn't). Your feedback helps us expand the tested battery list.
- Home Assistant 2024.1.0 or newer
- Battery on the same local network as Home Assistant
- Battery's static IP address and TCP port (typically 8080)
- Open HACS in Home Assistant
- Go to Integrations
- Click the three-dot menu > Custom repositories
- Add
https://github.com/StekkerDeal/aecc-battery-localas an Integration - Search for AECC Battery and click Download
- Restart Home Assistant
- Download the latest release from GitHub Releases
- Copy the
custom_components/aecc_batteryfolder into your Home Assistantconfig/custom_components/directory - Restart Home Assistant
- Go to Settings > Devices & Services > Add Integration
- Search for AECC Battery (Local TCP)
- Enter your battery's IP address, TCP port (default 8080), and a friendly name
- Select your battery brand from the dropdown (Lunergy, Sunpura, Voltdeer, AEG, Other)
- Optionally enter the model name (e.g. Hub 2400 AC, S2400, SR)
You can update all settings at any time via the integration's Configure button.
By default, the integration limits both charging and discharging to 800W. The hardware supports up to 2400W, and since charging and discharging have different constraints (feed-in rules and house wiring apply to output only), the limits are configured per direction via the integration's Configure button:
- Max charge power (100-2400W): the highest charging power the integration will command. Charging draws from the grid, so no feed-in limits apply.
- Max discharge power (100-2400W): the highest discharging power the integration will command.
All control paths enforce these limits: the Power Setpoint entity gets matching bounds (e.g. -800 to +2400 for an 800W discharge / 2400W charge configuration), and out-of-range slider or automation commands are clamped with a warning in the log.
Two caps govern output. The integration's discharge limit bounds what gets commanded (plus register 3039, the device's local power cap, which the integration raises automatically when either limit exceeds 800W). Separately, the AECC app has an "On Grid Output" setting (under Operating Mode Settings, factory default 800W) that caps what the inverter will actually deliver. That setting is not accessible via local TCP; for discharging above 800W it must be raised in the app once, per device.
Tip for limited circuits: to charge fast while keeping discharge safe, set Max charge power to 2400W, Max discharge power to 800W, and leave "On Grid Output" at 800W in the app. The integration then never commands more than 800W output, and the device itself enforces the same cap at the hardware level.
Disclaimer: Only raise the discharge limit above 800W when the battery is connected to its own dedicated circuit.
Configurations from before v1.5.2 migrate automatically: entries with the old "Extended power range" switch enabled get 2400W in both directions, all others keep 800W.
The AECC TCP protocol on some devices (notably Lunergy) occasionally returns sensor values that are physically impossible, for example SOC=0 while the battery is actively discharging at hundreds of watts. The underlying datalog gateway loses sync with the BMS and the JSON response defaults missing fields to 0 rather than marking them unavailable. Without protection these readings would pollute the Energy Dashboard accumulators and trigger automations on bogus thresholds.
The integration applies a small physics-aware filter before publishing readings to Home Assistant:
| Check | What it rejects |
|---|---|
| Zero-during-active-flow | SOC=0 (or power=0 on Lunergy) while the battery is clearly cycling |
| Rate-of-change | SOC changes faster than physically possible from one poll to the next |
When a reading is rejected, the entity holds its last known good value for up to 2 minutes so brief blips don't break charts or automations. After that window the entity transitions to unavailable so prolonged sensor failures surface honestly. As soon as the cleaner accepts a reading again, the entity returns to normal.
Per-brand thresholds. The brand you select during setup determines the cleaning sensitivity. Lunergy gets the strictest profile (the SOC-stuck-at-zero pattern is documented on this device). Sunpura, Voltdeer, and AEG get a permissive profile that only catches obvious physical impossibilities. Other uses a conservative middle setting. No user-facing configuration is needed.
Every control command (direction, power, work mode, SOC limits) is automatically re-read 0.5 seconds after writing. If the device reports a value that differs from what was requested, the integration logs a WARNING so silent firmware drops become visible. The write itself still returns success based on the SET response, the verification is best-effort and does not change the public API.
| Entity | Type | Description |
|---|---|---|
| Battery SOC | Sensor (%) | State of charge |
| Battery Power | Sensor (W) | Signed: positive = charging, negative = discharging |
| Battery Status | Sensor | Charging, Discharging, or Idle |
| Energy Charged | Sensor (kWh) | Accumulated charge energy (AC + PV), total_increasing |
| Energy Discharged | Sensor (kWh) | Accumulated discharge energy, total_increasing |
| Energy Generated | Sensor (kWh) | Accumulated PV energy, total_increasing |
| AC Charging Power | Sensor (W) | AC grid charging power |
| Battery Discharging Power | Sensor (W) | Discharge power |
| PV Power | Sensor (W) | Total solar power |
| PV Charging Power | Sensor (W) | Solar power charging battery |
| Grid / Meter Power | Sensor (W) | Smart meter reading |
| Grid Export Power | Sensor (W) | Power exported to grid |
| Backup Power | Sensor (W) | Backup/off-grid load power |
| PV String 1 Power | Sensor (W) | Individual PV string |
| PV String 2 Power | Sensor (W) | Individual PV string |
| Firmware Version | Sensor | Diagnostic; available on some AECC devices |
| WiFi Signal | Sensor (dBm) | Diagnostic; datalogger WiFi signal strength, available on some AECC devices. Refreshes about once a minute |
| Entity | Type | Description |
|---|---|---|
| Battery Direction | Select | Charge, Discharge, or Idle |
| Battery Power | Number (slider) | Power target magnitude, up to the higher of the two power limits |
| Power Setpoint | Number (box) | Signed one-write control: positive = charge, negative = discharge, 0 = idle. Bounds follow the per-direction power limits. Made for external energy managers |
| Discharge Limit | Number (slider) | Min SOC before discharge stops (5-50%) |
| Charge Limit | Number (slider) | Max SOC before charging stops (50-100%) |
| Work Mode | Select | Self-Consumption (AI), Custom/Manual |
Some AECC systems stack multiple batteries in a master/slave configuration. Add the integration once, pointing at the master's IP: the master reports data for the whole stack, and the slave does not serve the local API at all.
With 2 or more units, the integration creates:
- The main device with whole-system sensors (totals as computed by the master itself) and all controls
- One child device per battery ("Battery 1", "Battery 2", ...) with per-unit SOC, charging/discharging power, PV, backup power, and status
Controls stay on the main device only: the AECC protocol has no per-unit control; the master distributes one setpoint across the stack.
Single-unit systems are unaffected (no child devices). If you add or remove a battery from the stack, reload the integration to refresh the device list.
- Go to Settings > Dashboards > Energy
- In Battery Systems, click Add Battery System
- Energy going in: select
Energy Charged - Energy coming out: select
Energy Discharged - Click Save
Energy sensors use Riemann sum integration (the AECC TCP protocol does not expose cumulative counters). Values persist across restarts.
Two entities work together:
- Battery Direction (select): Charge, Discharge, or Idle
- Battery Power (slider): 0 up to the higher of the two configured power limits
Selecting a direction (or moving the Power slider) automatically switches to Custom mode and writes the schedule register. The Work Mode selector reflects this and shows Custom.
To stop the battery, set Battery Direction to Idle (or Power to 0). This holds an active 0 W setpoint. There is no separate "Disabled" mode, because turning EMS off does not reliably stop the battery (it hands control back to the device's own logic).
For automations and external controllers there is also Power Setpoint: a single signed number. Positive = charge, negative = discharge, 0 = idle, in watts. Writing it does the same as setting Direction + Power together (including the switch to Custom mode), but in one service call, so an automation can never race between the two writes. All control entities stay in sync whichever one you use.
| Mode | Description |
|---|---|
| Self-Consumption (AI) | Automatic charge/discharge based on solar and consumption |
| Custom / Manual | Manual control via Direction + Power |
- Discharge Limit: stops discharging at this SOC (default 10%)
- Charge Limit: stops charging at this SOC (default 98%)
This integration deliberately contains no planner: a home energy management system (HEMS) computes when to charge or discharge (from dynamic prices, PV forecast, load), and this integration executes it. Any HEMS that can call Home Assistant services works; EMHASS is the most complete open-source option, evcc (which can read HA entities directly) is a simpler threshold-based alternative.
Mind the sign conventions. The Power Setpoint entity uses positive = charge; EMHASS publishes its battery schedule (sensor.p_batt_forecast) with positive = discharge. Flip the sign in the automation:
automation:
- alias: "EMHASS: apply battery setpoint"
triggers:
- trigger: state
entity_id: sensor.p_batt_forecast
conditions:
- condition: template
value_template: "{{ trigger.to_state.state not in ('unknown', 'unavailable') }}"
actions:
- action: number.set_value
target:
entity_id: number.my_battery_power_setpoint
data:
# EMHASS: + = discharge. Setpoint: + = charge. Hence the minus.
value: "{{ -(states('sensor.p_batt_forecast') | float(0)) | round(0) }}"
- alias: "EMHASS: kill-switch (stale plan -> idle)"
triggers:
- trigger: state
entity_id: sensor.p_batt_forecast
to: ["unknown", "unavailable"]
for: "00:30:00"
actions:
- action: number.set_value
target:
entity_id: number.my_battery_power_setpoint
data:
value: 0The kill-switch matters: without it, a crashed optimizer leaves the last setpoint running indefinitely.
Configuration tips:
- Set EMHASS's battery bounds to match this integration:
battery_discharge_power_max/battery_charge_power_max= your Max discharge power / Max charge power settings, and keepbattery_minimum_state_of_charge/battery_maximum_state_of_chargein sync with the Discharge Limit / Charge Limit entities. - The device's own protection still applies: the battery stops at its SOC limits regardless of the commanded setpoint.
If you have an AECC-platform battery from a brand not yet listed as tested:
- Install the integration and select your brand (or "Other")
- Start with monitoring only, check that sensors return data and values look correct
- Only try control features after confirming sensors work
- Open an issue or PR to let us know your results
The integration writes the same registers as the official AECC app, but different devices may have firmware variations.
Entities show "Unavailable"
- Verify the battery IP and port:
ping <battery-ip>from your HA host - Check Home Assistant logs for connection errors
Controls have no effect
- The integration automatically sets Custom mode when you pick a direction
- Check logs for
SET battery_controlentries
Discharge/charge power capped below requested value
- Check the "On Grid Output" setting in the AECC app (Operating Mode Settings). This is a firmware-level cap that limits the maximum power the inverter delivers, regardless of what the integration requests. The factory default is 800W. Set it to your desired maximum (e.g. 2400W) in the app.
Energy sensors show 0 kWh after restart
- On first install, sensors start at 0 and accumulate
- After restart, last known values are restored automatically
When opening an issue, please attach a diagnostics export so we can see your device state without round-tripping for logs:
- Settings → Devices & Services → AECC Battery → ⋮ → Download Diagnostics
- Attach the resulting JSON file to the issue
The export contains the integration version, device model and firmware, configured brand profile, the last raw poll response, a fresh dump of control registers 3000-3130, and the last 20 control writes with their verify outcomes. Serial numbers and the local IP are redacted automatically.
If the bug involves a specific control flow (e.g. switching between work modes), capture one diagnostics file per step — the diff tells us which registers behave unexpectedly.
Based on Mathieuleysen/Sunpura-Local-TCP. Extended with multi-brand AECC support, energy dashboard sensors, battery control, and multi-language translations.
Maintained by StekkerDeal.
MIT, see LICENSE



