I have successfully integrated 2 SAX batteries in Home Assitant and I’m steering them with a few automations. Please chime in with further ideas. I still need to figure out some Modbus TCP particularities and fine-tune the setup, but it’s already working efficiently.
Those batteries have the advantage of not needing an additonnal inverter for AC/DC conversion, which is done within the battery pack in a digital way, which enables efficiencies. The battery is normally operated as a slave, steered by a smart meter. I decided to skip the smart meter setup and use my solaredge smartmeter that Home Assistant is already reading, in order to build a few automations the battery manufacturer would not integrate. After some exchange with their support team, they confirmed this was possible and I went on buying them and setting it up in HA. Most of my codes here were inspired by this german forum topic.
First step consisted in adding a modbus.yaml by adding this to my configuration.yaml
modbus: !include modbus.yaml
In this newly created mdobus.yaml, I’m defining all the entities of the battery, as per their MODBUS documentation:
- name: SAX_Battery_A
type: tcp
host: 192.168.x.xx1
port: 502
delay: 1
message_wait_milliseconds: 30
timeout: 5
switches:
- name: SAX_A_on_off
unique_id: "uid-sax-a-on-off"
address: 45
slave: 64
write_type: holdings
command_on: 2
command_off: 1
verify:
address: 45
state_on: 3
state_off: 1
sensors:
- name: SAX_status_A
address: 45
slave: 64
- name: SAX_SOC_A
device_class: battery
state_class: measurement
slave: 64
address: 46
- name: SAX_power_A
unit_of_measurement: W
device_class: power
state_class: measurement
slave: 64
address: 47
offset: -16384
- name: SAX_Smartmeter_A
unit_of_measurement: W
device_class: power
state_class: measurement
slave: 64
address: 48
offset: -16384
- name: SAX_a_Capacity
unique_id: "uid-sax-a-capacity"
unit_of_measurement: "Wh"
device_class: energy
state_class: measurement
slave: 40
address: 40115
scan_interval: 120
data_type: int16
scale: 10
- name: SAX_a_cycles
unique_id: "uid-sax-a-cycles"
state_class: measurement
slave: 40
address: 40116
scan_interval: 3600
data_type: int16
- name: SAX_a_Temp
unique_id: "uid-sax-a-temp"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
slave: 40
address: 40117
scan_interval: 120
data_type: int16
- name: SAX_a_energy_produced
unique_id: "uid-sax-a-energy_produced"
unit_of_measurement: "Wh"
device_class: energy
state_class: total
slave: 40
address: 40096
scan_interval: 120
data_type: uint16
- name: SAX_a_energy_consumed
unique_id: "uid-sax-a-energy_consumed"
unit_of_measurement: "Wh"
device_class: energy
state_class: total
slave: 40
address: 40097
scan_interval: 120
data_type: uint16
- name: SAX_Battery_B
type: tcp
host: 192.168.x.xx2
port: 502
delay: 1
message_wait_milliseconds: 30
timeout: 5
switches:
- name: SAX_B_on_off
unique_id: "uid-sax-b-on-off"
address: 45
slave: 64
write_type: holdings
command_on: 2
command_off: 1
verify:
address: 45
state_on: 3
state_off: 1
sensors:
- name: SAX_status_B
address: 45
slave: 64
- name: SAX_SOC_B
device_class: battery
state_class: measurement
slave: 64
address: 46
- name: SAX_power_B
unit_of_measurement: W
device_class: power
state_class: measurement
slave: 64
address: 47
offset: -16384
- name: SAX_Smartmeter_B
unit_of_measurement: W
device_class: power
state_class: measurement
slave: 64
address: 48
offset: -16384
- name: SAX_b_Capacity
unique_id: "uid-sax-b-capacity"
unit_of_measurement: "Wh"
device_class: energy
state_class: measurement
slave: 40
address: 40115
scan_interval: 120
data_type: int16
scale: 10
- name: SAX_b_cycles
unique_id: "uid-sax-b-cycles"
state_class: measurement
slave: 40
address: 40116
scan_interval: 3600
data_type: int16
- name: SAX_b_Temp
unique_id: "uid-sax-b-temp"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
slave: 40
address: 40117
scan_interval: 120
data_type: int16
- name: SAX_b_energy_produced
unique_id: "uid-sax-b-energy_produced"
unit_of_measurement: "Wh"
device_class: energy
state_class: total
slave: 40
address: 40096
scan_interval: 120
data_type: uint16
- name: SAX_b_energy_consumed
unique_id: "uid-sax-b-energy_consumed"
unit_of_measurement: "Wh"
device_class: energy
state_class: total
slave: 40
address: 40097
scan_interval: 120
data_type: uint16
With this setup, we now have some on/off switches for each battery and all the sensors should already send back the values.
Now to stear the battery, there are a few prerequisites:
- customer service from the battery manufacturer needs to allow to write the modbus registers. A simple email solved this
- a few helpers and automation need to be setup to enable setting the charging/discharging of the battery
First, the helpers, they were created in the UI, so you can find them in /.storage/core.config_entries
The first helper is a value template to calculate at what power the battery should charge or discharge, reading from the solaredge smartmeter value and deducting the power consumed or produced by the 2 batteries, capping the value at its maximum power (it runs on a dedicated 20A line, so 4600W each, combined at 9200W).
Update: found out via customer service that maximum charge rate is at 3500W per Battery while max discharge was at 4600W, so total charging for my 2 batteries at 7000W and discharge unchanged at 9200W.
The second is the cos(phi) or power factor reading from solardege, transformed in the right format for the battery to accept it as a value.
The third calculates the total power output or input of both batteries to know the combined production or consumption of electricity in one value.
{"created_at":"2025-01-23T21:01:36.519135+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JJAFGVC7VHKJ4VRC1H2VPB0A","minor_version":1,"modified_at":"2025-01-25T16:52:35.622294+00:00","options":{"max":9200.0,"min":-9200.0,"name":"sax_power_calc","set_value":[],"state":"{% set p= ((-(states('sensor.solaredge_m1_ac_power')| float(0)) + states(\"sensor.sax_power_a\")| float(0) + states(\"sensor.sax_power_b\")| float(0) ) | round(0)) %} \n{% if 9200 < p %} 9200\n{% elif -9200 > p %} -9200\n{% else %} {{(-(states('sensor.solaredge_m1_ac_power')| float(0)) + states(\"sensor.sax_power_a\")| float(0) + states(\"sensor.sax_power_b\")| float(0) ) | round(0)}}\n{% endif %}","step":1.0,"template_type":"number","unit_of_measurement":"W"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","title":"sax_power_calc","unique_id":null,"version":1},
{"created_at":"2025-01-23T21:20:40.930417+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JJAGKRZ2K1D9FRH0SVTJ833Y","minor_version":1,"modified_at":"2025-01-23T21:20:40.930425+00:00","options":{"max":1000.0,"min":-1000.0,"name":"sax_cos_phi_calc","set_value":[],"state":"{{(states.sensor.solaredge_m1_ac_pf.state | float(0) *10 )| round(0) | int }}","step":1.0,"template_type":"number"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","title":"sax_cos_phi_calc","unique_id":null,"version":1},
{"created_at":"2025-01-25T17:33:24.351295+00:00","data":{},"disabled_by":null,"discovery_keys":{},"domain":"template","entry_id":"01JJF8D1ZZC2B8DHXQBHFM2Z34","minor_version":1,"modified_at":"2025-01-25T17:33:24.351303+00:00","options":{"max":9200.0,"min":0.0,"name":"SAX_combined_power","set_value":[],"state":"{{ states('sensor.sax_power_a')| float(0) + states('sensor.sax_power_b')| float(0) }}","step":1.0,"template_type":"number","unit_of_measurement":"W"},"pref_disable_new_entities":false,"pref_disable_polling":false,"source":"user","title":"SAX_combined_power","unique_id":null,"version":1}
Then, a few boolean need to be created (again through the UI), visible here:
One to enable the battery optimisation of charging/discharging and I have created a second one to enable a full power charge of the battery, whatever condition exists.
{
"version": 1,
"minor_version": 1,
"key": "input_boolean",
"data": {
"items": [
{
"icon": "mdi:battery-charging",
"name": "full charge",
"id": "full_charge"
},
{
"id": "sax_solarcharge_on_off",
"icon": "mdi:battery-sync",
"name": "sax_solarcharge_on_off"
},
{
"id": "sax_full_power_charge",
"name": "SAX full power charge",
"icon": "mdi:battery-charging-high"
}
]
}
}#
and some inpute numbers to have a slider with max charge and discharge number, which can be then set on the battery controller:
{
"id": "sax_discharge",
"min": 0.0,
"max": 9200.0,
"name": "sax_max_discharge",
"icon": "mdi:battery-arrow-down",
"mode": "slider",
"step": 1.0,
"unit_of_measurement": "W"
},
{
"id": "sax_charge",
"min": 0.0,
"max": 7000.0,
"name": "sax_max_charge",
"icon": "mdi:battery-arrow-up",
"mode": "slider",
"step": 1.0,
"unit_of_measurement": "W"
}
Now that all those values are setup, it’s time to move to automations, stored in automations.yaml
:
1 automation will set the charge/discharge level of the battery, if the boolean is enabled.
Also, it will check if my Tesla is at home and charging, in which case I move the battery charge and discharge to 0 to give priority to the car charging. Of course, if the status of the car changes, it will need to adjust the battery charging. This is the best I figured out so far, but I would defnitely need to make some optimisation, taking into account level of charge of batteries or more factors.
- id: '1737666225568'
alias: Sax_power_calc_automation
description: ''
triggers:
- trigger: state
entity_id:
- input_boolean.sax_solarcharge_on_off
to: 'on'
enabled: true
- trigger: time_pattern
minutes: /1
conditions:
- condition: state
entity_id: input_boolean.sax_solarcharge_on_off
state: 'on'
actions:
- if:
- condition: and
conditions:
- condition: state
entity_id: device_tracker.toune_location_tracker
state: home
- condition: state
entity_id: switch.toune_charger
state: 'on'
then:
- action: modbus.write_register
metadata: {}
data:
hub: SAX_Battery_A
slave: 64
address: 41
value: 0
else:
- action: modbus.write_register
metadata: {}
data:
hub: SAX_Battery_A
address: 41
slave: 64
value: '[{{states.number.sax_power_calc.state|int|bitwise_and(0xffff)}},{{states.number.sax_cos_phi_calc.state|int|bitwise_and(0xffff)}}]'
mode: single
- id: '1737669562480'
alias: SAX set max discharge from slider
description: ''
triggers:
- trigger: state
entity_id:
- input_number.sax_discharge
conditions: []
actions:
- action: modbus.write_register
metadata: {}
data:
hub: SAX_Battery_A
address: 43
slave: 64
value: '[{{states.input_number.sax_discharge.state|int|bitwise_and(0xffff)}}]'
mode: single
- id: '1737669689038'
alias: SAX_set_max_charge from slider
description: ''
triggers:
- trigger: state
entity_id:
- input_number.sax_discharge
conditions: []
actions:
- action: modbus.write_register
metadata: {}
data:
hub: SAX_Battery_A
address: 44
slave: 64
value: '[{{states.input_number.sax_charge.state|int}}]'
mode: single
- id: '1737670285283'
alias: SAX threshold discharge and reset up
description: ''
triggers:
- entity_id:
- sensor.SAX_SOC_A
id: SOC_below_10
below: 10
for:
hours: 0
minutes: 1
seconds: 0
enabled: true
trigger: numeric_state
- entity_id:
- sensor.SAX_SOC_A
id: SOC_below_20
for:
hours: 0
minutes: 5
seconds: 0
below: 20
enabled: true
trigger: numeric_state
- entity_id:
- sensor.SAX_SOC_A
id: SOC_above_20
for:
hours: 0
minutes: 5
seconds: 0
enabled: true
trigger: numeric_state
above: 20
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id:
- SOC_below_10
sequence:
- action: input_select.select_option
target:
entity_id: input_select.sax_a_max_discharge_power
data:
option: '0'
- conditions:
- condition: trigger
id:
- SOC_below_20
sequence:
- action: input_select.select_option
target:
entity_id: input_select.sax_a_max_discharge_power
data:
option: '500'
- conditions:
- condition: trigger
id:
- SOC_above_20
sequence:
- action: input_select.select_option
target:
entity_id: input_select.sax_a_max_discharge_power
data:
option: '9200'
mode: single
- id: '1737877813702'
alias: SAX full power charge
description: ''
triggers:
- trigger: state
entity_id:
- input_boolean.sax_full_power_charge
from: 'off'
to: 'on'
id: sax_off_to_on
- trigger: state
entity_id:
- input_boolean.sax_full_power_charge
from: 'on'
to: 'off'
id: sax_on_to_off
conditions: []
actions:
- choose:
- conditions:
- condition: trigger
id:
- sax_off_to_on
sequence:
- action: input_boolean.turn_off
metadata: {}
data: {}
target:
entity_id: input_boolean.sax_solarcharge_on_off
- action: modbus.write_register
metadata: {}
data:
hub: SAX_Battery_A
address: 41
slave: 64
value: '[{{(0-(states.input_number.sax_charge.state)| float(0))|int|bitwise_and(0xffff)}}]'
- conditions:
- condition: trigger
id:
- sax_on_to_off
sequence:
- action: input_boolean.turn_on
metadata: {}
data: {}
target:
entity_id: input_boolean.sax_solarcharge_on_off
mode: single
A few important notes here, Modbus HEX format does not accept negative value, so changing the format with |bitwise_and(0xffff)
enables a negative number transmission.
At this stage, the max discharge and charge level setup are not accepted by the system, no error but the setting of the power overwrites it. Maybe my register was not properly enabled, as the documentation states that in terms of priority, limitation of power should overrule the power set through modbus. More to come.
Update: The below part is solved in the next post
Another open point is the setting of the power should normally be combined into one Modbus call. I could not figure out how to set 2 registers in Modbus in one single call. The battery accepts it in its documentation, but I’m unsure if Modbus HA module can do this.
For exemple, in my code, I pass first the value of the desired power, wait 30 seconds and pass the cos(phi). In the documentation of the batteries, this could be done in one call. Here is the HEX output.
Write on Register 41, slave 64 a power value of 500W is written like this:
00 01 00 00 00 09 40 10 00 29 00 01 02 01 f4
and simultaneously writing Register 41 and 42 with values of 500W and a cos(phi) of 100% (transformed in the proper format to 1000):
00 01 00 00 00 0b 40 10 00 2b 00 02 04 01 f4 03 e8
Could not find how to do this in HA. Any clue?