Skip to content

Latest commit

Β 

History

33 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”‹ AECC Balcony Battery ESPHome Component

Control an AECC-platform balcony battery from Home Assistant, locally, over RS485.

These all-in-one batteries are sold under many names β€” Sunpura, Lunergy, AEG Solarcube, Voltdeer, AFERIY, AccuMate, JET GreenARK, Oscal, Fossibot, Humsienk. Underneath they run the same platform.

Normally you configure them through the vendor's phone app, which talks to their cloud. This component talks to the battery directly instead.

✨ What you get

  • Live readings in Home Assistant with no configuration: state of charge, battery power, grid power, load
  • Every setting the battery has, including ones the app asks for a password
  • A power setpoint, so it replaces the vendor's integration rather than sitting alongside it
  • Zero-export control β€” hold your grid connection at a target, for places where feeding in is not allowed
  • A configuration backup you can download and put back

πŸ”Œ Wiring

You need an ESP32 and one RS485 adapter for the inverter. Add a second adapter only if you want zero-export control, which needs a meter on its own bus. A plain ESP32 is the safe choice: it has three serial ports.

Buy auto-direction adapters, the kind with only VCC, GND, TXD and RXD. The component has no transmit-enable pin, so a breakout that brings out DE/RE will not transmit.

Use a normal patch cable into the battery, then break out only two conductors at your end, with a keystone jack or by cutting the cable:

RJ45 pin 7  β†’  A
RJ45 pin 8  β†’  B

Do not connect the other six. The socket carries more than one RS485 bus, and one of them is the live link between the inverter and its battery pack.

Power the adapters from 3.3 V, not 5 V. Many of them pass their supply voltage straight through to the TTL side, and a 5 V RXD output is above what an ESP32 pin is rated for. If you are not sure, measure RXD against ground before connecting it: RS485 idles high, so it will read either 3.3 V or 5 V and tell you.

⚠️ Check the pinout in your own manual. The pins above are from a Humsienk HSBPSS2K5W7K68WHEU. Other brands on this platform are probably the same, but a wrong guess here lands on the battery's own BMS link.

The bundled CT meter is a second bus, on its own adapter:

RJ45 pin 2 or 4  β†’  A
RJ45 pin 3 or 5  β†’  B

It bridges each net across two pins internally, which is why a straight patch cable between the meter and the battery shorts the two together.

⚠️ If you wire a meter, isolate one of the two buses. The meter and the battery are powered separately. Two plain adapters sharing the ESP32's ground tie their electrical references together, which RS485 will not tolerate for long.

πŸš€ Quick start

external_components:
  - source:
      type: git
      url: https://github.com/makstech/esphome-aecc
      ref: main

uart:
  - id: bus_inverter
    tx_pin: GPIO32
    rx_pin: GPIO33
    baud_rate: 9600

aecc:
  id: nova
  uart_id: bus_inverter

That is the whole configuration. It gives you the battery's readings, every setting it has, and a setpoint to drive it with β€” enough to replace the vendor's own integration.

πŸ“Š What you get in Home Assistant

These appear on their own. A reading is created when the thing it describes is configured, so a setup without a meter has no meter readings.

Readings

Key Name What it is
soc_sensor Battery SOC State of charge, %
battery_power_sensor Battery power Positive when discharging
grid_power_sensor Grid power At the battery's own connection, positive when exporting
backup_load_sensor Backup load Load on the battery's own socket
setpoint_sensor Setpoint What the battery has been told to do
ac_charge_power_sensor AC charge power Drawn from AC to charge the battery, including from AC-coupled PV; zero while discharging
pv_power_sensor PV power Into the battery's PV port, positive while producing

Energy β€” always, for Home Assistant's Energy dashboard

Key Name What it is
energy_charged_sensor Energy charged Energy into the battery, kWh, integrated on the device from battery power every 5 s and kept across reboots
energy_discharged_sensor Energy discharged Energy out of the battery, the same way

Control diagnostics β€” with control:, and the meter ones also with meter:

Key Name What it is
meter_power_sensor Meter grid power What your meter is reading right now
meter_power_filtered_sensor Meter grid power, filtered The same after smoothing, which is what the loop acts on
commanded_power_sensor Commanded power What the loop is asking the battery for
meter_age_sensor Meter sample age Seconds since the last good meter reading
loop_rate_sensor Control loop rate How often the loop is actually running, Hz

Health

Key Name On means Needs
control_effective_sensor Setpoint honoured The battery is obeying the setpoint control:
meter_ok_sensor Meter OK The meter is answering with fresh readings meter: + control:
ems_ready_sensor Scheduler ready The battery's scheduler is set up for local control datalogger: + control:

Setpoint honoured goes off while the mode is Off. The meter is read in every mode, so it doubles as a grid power reading even when nothing is regulated.

To rename or reconfigure one, name its key under aecc::

aecc:
  soc_sensor: Charge level
  meter_age_sensor:
    name: Meter age
    interval: 30s

tests/all-keys.yaml is generated from the component's schema and names every key it accepts. CI fails when the schema gains a key that it, or this README, does not name; the options outside these tables are in All options.

Battery settings β€” also under aecc:, see below

Other β€” a Back up configuration button appears with backup:, and the aecc.write_register action writes any address.

βš–οΈ Modes

A Battery mode dropdown decides what the component does, and it remembers your choice across reboots:

Mode What it does
Off Rests the battery. The component then writes nothing, so the vendor app, the cloud or anything else on the bus can take over
Zero export Holds your grid connection at a target, charging rather than exporting
Manual Holds the battery at a power you set, positive discharging

Off is the default. Switching to it from Zero export or Manual turns the battery's energy manager off and sets its priorities to Photovoltaic priority with PV-only charging, once: the battery rests, the house runs on PV and then the grid, and the pack charges from PV surplus alone. After that the component writes nothing, so it will not fight you while you drive the battery some other way. Without a datalogger: it only stops writing. Zero export only appears in the list once you have given the component a meter:; Manual is there from the start, driven by the Battery power setpoint. Manual at 0 W parks the battery idle while the ESP32 stays in control.

Handing the battery back

A datalogger: block also gives you a Work mode dropdown, for handing the battery back to its built-in self-consumption automation while in Off. Choosing either work mode turns the energy manager back on. Name it with work_mode_select: to change the label.

Battery mode Work mode What runs the battery
Off Self-consumption The battery's own automation
Off, after Zero export or Manual unchanged Nothing: the energy manager is off and the battery rests
Off Custom Nothing; it holds the resting slot, a slow charge
Zero export or Manual Custom This component

Self-consumption can only be selected while the battery mode is Off, because the other two modes need Custom and put it back. Note that the battery's automation rewrites the schedule slot and the SOC limits to suit itself, and it is free to export β€” so it is not a substitute for zero export where feeding in is not allowed.

uart:
  - id: bus_inverter
    tx_pin: GPIO32
    rx_pin: GPIO33
    baud_rate: 9600
  - id: bus_meter          # only with a meter
    tx_pin: GPIO25
    rx_pin: GPIO26
    baud_rate: 9600

aecc:
  id: nova
  uart_id: bus_inverter
  meter:
    type: rs071        # the CT meter that came in the box
    uart_id: bus_meter
  datalogger:
    host: 192.0.2.10   # the battery's own WiFi module, on your network
  control:
    setpoint_number: Battery power   # the Manual target; negative charges
    grid_target: 60    # watts to keep importing, in Zero export
    max_discharge: 600
    max_charge: 2400
    min_soc: 15%
    max_soc: 90%

min_soc and max_soc apply in Manual as well, so a bad setpoint cannot discharge past your reserve, and so do max_discharge and max_charge β€” a Manual setpoint beyond them is held at the limit and logged.

The datalogger: block is not optional in practice. Without it the battery ignores the control loop and keeps running whatever the vendor app last scheduled, while every control entity still appears in Home Assistant. Commissioning explains why.

Picking a mode is what starts any of this; a freshly flashed node sits idle.

Set max_discharge near your usual household draw. If the load suddenly drops, the battery takes about a second to wind down, and how much it pushes out during that second is roughly the difference between this number and your baseline.

grid_target is how much you keep importing rather than sitting exactly on zero. The Home Assistant control stops at zero, so an optimiser cannot ask the battery to export by accident. Give grid_target_number a negative min_value where exporting is wanted and permitted.

Letting Home Assistant steer it

Name the loop's own parameters and they become controls too, starting from the values you set above. The mode dropdown is there whether you name it or not; naming it only changes the label:

  control:
    grid_target: 60
    max_discharge: 600
    mode_select: Battery mode
    grid_target_number: Grid target
    max_discharge_number: Max discharge
    max_charge_number: Max charge
    min_soc_number: Reserve
    max_soc_number: Charge ceiling

These remember their last value across a reboot, so something like EMHASS can change them by the hour:

What you want grid target max discharge max charge
Soak up your own solar 0 0 full
Charge from cheap grid at 2 kW 2000 0 full
Run the house off the battery 0 full 0
Save the battery for later 0 0 0

🧰 Battery settings

Every setting the battery has is already a control β€” the register, the range, the step and the unit are known, so nothing has to be listed.

The ones you might change appear in Home Assistant under Configuration: the grid export limit, max charge current, energy saving, the buzzer, and the state-of-charge thresholds. The rest are commissioning settings β€” pack chemistry, grid nominals, BMS protocol β€” and stay off the dashboard. They still exist: YAML, automations and aecc.write_register all reach them.

To put them all in Home Assistant:

aecc:
  expose_all_settings: true

Or move a single one either way with ESPHome's own internal:, which wins over that:

aecc:
  grid_standard_select:
    internal: false     # this one on the dashboard
  buzzer_mute_switch:
    internal: true      # this one off it

Name one to change its label, or to override a default:

aecc:
  id: nova
  uart_id: bus_inverter
  buzzer_mute_switch: Beeper
  soc_shutdown_number:
    name: Shut down at
    min_value: 5
    max_value: 50

The suffix says what kind of control you get: _number for a value, _switch for an on/off setting, _select for a list of choices.

Suffix Available for
_number grid_export_limit, output_voltage, output_frequency, max_charge_current, pv_max_charge_current, mains_max_charge_current, saturation_current, cv_voltage, float_voltage, cv_charge_time, cv_return_voltage, battery_to_mains_voltage, mains_to_battery_voltage, undervoltage_alarm, low_voltage_shutdown, eod_voltage, shutdown_delay, eod_clear_voltage, soc_low_alarm, soc_shutdown, soc_full, soc_inverter_to_mains, soc_mains_to_inverter
_switch device_power, eco_mode, buzzer_mute, battery_activation, mixing_priority, external_ct_host, anti_islanding, bms_function, independent_pack
_select utility_range, on_grid_mode, grid_standard, inverter_mode, charge_priority, battery_type, parallel_mode, bms_protocol

Some do more than they sound like. device_power switches the inverter off. anti_islanding off stops it disconnecting from a dead grid. bms_function off drops the pack's BMS link. grid_export_limit caps on-grid output, which includes power serving your own load, so setting it to zero stops the battery supplying the house rather than stopping export. And grid_standard swaps the inverter's whole set of grid voltage and frequency trip limits in one write.

These are the battery's own settings, so they are read back from it at boot rather than remembered here β€” change one in the vendor app and the entity follows.

A few settings have no key on purpose β€” the enums whose value lists were never worked out, where a bare index would be a worse control than none. Those and anything else are still reachable by address, and docs/REGISTERS.md lists every register with the key that reaches it, where there is one:

  registers:
    - name: Parallel mode
      address: 0xA02C
      max_value: 7

Give it a name: for it to appear in Home Assistant.

πŸ”§ Commissioning

The battery ignores the component until its own scheduler is set up, and the vendor app undoes that whenever its AI mode is on. Give the component the battery's address and it handles this for you, checking every minute and putting it back if it drifts:

aecc:
  datalogger:
    host: 192.0.2.10     # the battery's own WiFi module, on your network
    resting_power: -50
    resting_power_number: Resting power   # there by default; name it to relabel

host: also takes a hostname or an mDNS name, so a battery on DHCP does not need a reservation:

  datalogger:
    host: humsienk-control.local

A Datalogger address text and a Datalogger switch come with the block. The first changes the address without reflashing; the second stops the component using the datalogger, for when the vendor app or another integration needs it. Name them with host_text: and enable_switch: to change the labels.

host: can be left out entirely, which is the point of the text entity β€” flash the node, then type the address into Home Assistant once you know it:

aecc:
  id: nova
  uart_id: bus_inverter
  datalogger:

Why resting_power cannot be zero

The battery only accepts commands while its own scheduler is running a non-zero value. Set that value to zero and it ignores the ESP32 completely.

That same value is also what the battery falls back to a few seconds after the ESP32 goes quiet. So one number does two jobs, and zero breaks both of them.

A small charging value is the quietest non-zero option, and charging can never push anything into the grid. Too small counts as zero, though: the battery treats -10 as nothing at all. The default -50 is about the smallest it listens to. To park the battery idle, use Manual at 0 W rather than a smaller resting value. Zero export rounds a smaller charge up to 50 W for the same reason, so a small PV surplus does not leak out.

The datalogger also pushes that value into the battery every few seconds, over whatever the ESP32 last commanded. So between control ticks the ESP32 checks its setpoint every 100 ms and puts it back when a push has overwritten it, then ignores the meter for a moment: the blip that follows is the push, not the house. Export larger than the push could have caused is still corrected at once. In Zero export the component also turns the scheduler's custom mode off, so the push carries the battery's own zero-import target instead of the resting value, and that is what the battery keeps doing if the ESP32 stops. Manual turns it back on.

The Setpoint honoured sensor turns off if the battery stops obeying. Put it on a dashboard or an automation to know when that happens. It reports on the control loop, so it needs control: as well.

πŸ’Ύ Backup and restore

Take a copy before you change anything on a new battery.

web_server:
  version: 3

aecc:
  backup:
    url: /aecc/backup
curl http://your-node.local/aecc/backup                 # starts it
sleep 60
curl http://your-node.local/aecc/backup -o backup.txt   # again, for the file

The first call only starts the walk; at 9600 baud it takes the best part of a minute.

Or press the Back up configuration button and fetch it afterwards. Putting it back:

curl --data-binary @backup.txt http://your-node.local/aecc/restore
curl -X POST -d '' http://your-node.local/aecc/restore     # for the report

Restore writes the settings and the energy manager's own values, skips anything already correct, and stops if the battery starts refusing. It does not switch the battery on, so on a completely blank unit that last step is still yours.

πŸ“ˆ Traces

For tuning the loop, trace: records what it sees, one sample per control tick, and serves the last recording as CSV:

aecc:
  trace:
    export_trigger: 30   # optional: export above 30 W records the moment by itself

Press Record trace, do whatever you want to see (step the Manual setpoint, switch a load on), then fetch http://<device>/aecc/trace once the recording ends. A recording starts with the five seconds before it was asked for, at negative timestamps. With export_trigger, export above that many watts records those five seconds and five more, at most once a minute, so it is there to fetch after the fact.

Each row has a timestamp in milliseconds from the trigger, the meter reading, the inverter's battery, grid-port and backup power, the setpoint register as the inverter holds it, the loop's command, the mode (0 Off, 1 Zero export, 2 Manual), the PV port's power and 1 where the datalogger pushed its own setpoint since the row before. A setpoint that differs from the command is something else writing to the battery. It needs web_server: and control:.

πŸ“‹ All options

Everything the component accepts besides the entity tables and the battery settings, with its default.

aecc:

Key Default What it does
uart_id required The bus wired to the inverter's RJ45 pins 7/8
unit 1 The inverter's Modbus address
expose_all_settings false Also shows the commissioning settings, see Battery settings
expose_tuning false Also shows the control loop's tuning controls
work_mode_select Work mode The Work mode dropdown; needs datalogger:
registers none Settings by address, see Battery settings
meter, control, datalogger, backup, trace The blocks below

meter: β€” without it there is no Zero export

Key Default What it does
type required rs071, the CT meter that came in the box
uart_id required The meter's own bus
unit 1 The meter's Modbus address
register 12 The float32 register holding grid power, positive when importing
reply_window 120ms How long to wait for an answer, up to 1 s

control:

Key Default What it does
rate 4Hz How often the loop runs; the meter refreshes about every 250 ms
filter_window 750ms The loop acts on the median meter reading over this long
grid_target 60 Watts to keep importing in Zero export
max_discharge 600 Watts, in Zero export and Manual
max_charge 2400 Watts, in Zero export and Manual
min_soc 15% The reserve: no discharging below it
max_soc 90% The ceiling: no charging above it
charge_taper soc and max_charge points the charge limit follows as the pack fills: a straight line between points, the last one held above it, no limit below the first. Under a high charge current the highest cell reaches the BMS's full voltage early and the BMS resets the pack to 100 %. It limits PV absorption too, so a surplus above it near the top needs curtailing
ramp_up 0.35 The share of an import error closed per half second while raising discharge. A move toward export is corrected in full at once
stale_after 5s With no meter reading for this long, the loop commands 0
law predictive How the error becomes a command. classic adds a share of it to the last command each tick; predictive adds it to what a model of the inverter says the battery is delivering, so it does not ask twice for power already on its way
predictive_gain 0.7 The share of an import error the predictive law corrects per tick; a move toward export is corrected whole
actuator_lag 400ms The predictive model: how fast the inverter follows a new setpoint
meter_delay 300ms The predictive model: how late the meter shows it. Too long double-counts and oscillates
rise_delay 2000ms How long import has to persist before zero export covers it, so a brief load pulse does not leave a spike of export when it stops. Export is corrected at once
pv_feed_forward true With the predictive law, a rise on the battery's PV port is absorbed as the inverter reports it, before the meter shows it as export. A fall is left to the meter
mode_select Battery mode The mode dropdown
setpoint_number Battery power The Manual target in watts, positive discharging
grid_target_number Grid target grid_target as a control, starting from the configured value
max_discharge_number Max discharge The same for max_discharge
max_charge_number Max charge The same for max_charge
min_soc_number Reserve The same for min_soc
max_soc_number Charge ceiling The same for max_soc
law_select Control law law as a dropdown, starting from the configured value; shown with expose_tuning
rate_number Loop rate rate as a control, for tuning live; shown with expose_tuning
filter_window_number Filter window The same for filter_window
predictive_gain_number Predictive gain The same for predictive_gain
meter_delay_number Meter delay The same for meter_delay
rise_delay_number Rise delay The same for rise_delay

datalogger:

Key Default What it does
host empty Address, hostname or mDNS name of the battery's WiFi module. Empty leaves it to the text entity
port 8080 Its TCP port
resting_power -50 Watts the schedule slot holds, which the battery falls back to when the loop stops. Must be negative, see below
reconcile_interval 60s How often the scheduler settings are checked and put back; every 5 s until they first hold
host_text Datalogger address Changes the address without reflashing
enable_switch Datalogger Stops all traffic to the datalogger, freeing it for the vendor app
resting_power_number Resting power resting_power as a control. A change is written to the slot at once, in Off too, since the slot is what the battery runs there

backup:

Key Default What it does
url /aecc/backup Where the backup is served
restore_url /aecc/restore Where a backup is posted to restore it
button Back up configuration Takes a backup

trace: β€” see Traces

Key Default What it does
url /aecc/trace Where the last trace is served
duration 30s How long a recording from the button runs, up to 2 minutes
export_trigger Export above this many watts records five seconds either side of it, at most once a minute
button Record trace Starts a recording

Numbers show as a number box unless their unit is a percentage; set mode: on one to override.

πŸ“– More

⚠️ Notes

Writing settings on a grid-connected battery can make it export, which needs permission in many places. Know your local rules.

Not affiliated with any of the brands listed.

About

Local ESPHome control for AECC-platform balcony batteries

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages