Integration of a SAX battery over Modbus TCP and automation for solar charging

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:

  1. customer service from the battery manufacturer needs to allow to write the modbus registers. A simple email solved this
  2. 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?

1 Like

Quick update on this one, as i have solved writing multiple consecutive registers by passing an array of values, in brackets, separated by commas.

 - 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)}}]

So writing starts at register 41 with the first value of my array, then the next value in my array sets the next register. I did not understand this properly in the modbus documentation, but that saves network traffic and avoids congestion on the Modus interface. I believe the order might depend on the CPU.

For those following here, I took some time to optimize the automations above.

  1. I systematically update 41,42,43,44 every 2 minutes with all current data, as the system will “forget” the data with no input after a certain time on those registers (I have read elsewhere something like 40 minutes)

  2. Added cases when battery falls below threshold to stop discharging, as when you are writing on register 41, the battery will let you go down to 0. For safety, capped charging when SOC is above 99

  3. created the case when car is at home and charging to not discharge nor charge the battery

  4. created an automation that manually charges the battery to full power, which is activated by a switch. I have as well an automation for testing purposes where i can play with by inputing any number.

(there is a bit of redundancy in this code as I limit discharging with register 43 as well as include it in the calculation of register 41, as it took me some time to figure out the most efficient way)

- id: '1736693092678'
  alias: SAX discharge to zero when car Toune tesla charging at home
  description: ''
  triggers:
  - trigger: state
    entity_id:
    - switch.toune_charger
    from: 'off'
    to: 'on'
    id: toune_charge_on
  - trigger: state
    entity_id:
    - switch.toune_charger
    from: 'on'
    to: 'off'
    id: toune_charge_off
  conditions:
  - condition: state
    entity_id: device_tracker.toune_location_tracker
    state: home
  actions:
  - choose:
    - conditions:
      - condition: trigger
        id:
        - toune_charge_on
      sequence:
      - action: input_number.set_value
        metadata: {}
        data:
          value: 0
        target:
          entity_id: input_number.sax_discharge
    - conditions:
      - condition: trigger
        id:
        - toune_charge_off
      sequence:
      - action: input_number.set_value
        metadata: {}
        data:
          value: 9200
        target:
          entity_id: input_number.sax_discharge
  mode: single
- 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: or
      conditions:
      - condition: and
        conditions:
        - condition: numeric_state
          entity_id: number.sax_power_calc
          above: 0
        - condition: numeric_state
          entity_id: sensor.sax_soc_a
          below: 15
      - condition: and
        conditions:
        - condition: state
          entity_id: device_tracker.toune_location_tracker
          state: home
        - condition: state
          entity_id: switch.toune_charger
          state: 'on'
      - condition: and
        conditions:
        - condition: numeric_state
          entity_id: number.sax_power_calc
          below: 0
        - condition: numeric_state
          entity_id: sensor.sax_soc_b
          above: 99
    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)}},{{states.input_number.sax_discharge.state|int|bitwise_and(0xffff)}},{{states.input_number.sax_charge.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_charge
  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: 5
      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
    enabled: true
    trigger: numeric_state
    above: 20
  conditions: []
  actions:
  - choose:
    - conditions:
      - condition: trigger
        id:
        - SOC_below_10
      sequence:
      - action: input_number.set_value
        metadata: {}
        data:
          value: 0
        target:
          entity_id: input_number.sax_discharge
    - conditions:
      - condition: trigger
        id:
        - SOC_below_20
      sequence:
      - action: input_number.set_value
        metadata: {}
        data:
          value: 250
        target:
          entity_id: input_number.sax_discharge
    - conditions:
      - condition: trigger
        id:
        - SOC_above_20
      sequence:
      - action: input_number.set_value
        metadata: {}
        data:
          value: 9200
        target:
          entity_id: input_number.sax_discharge
  mode: single
- id: '1737824867210'
  alias: sax set power BACKUP USE CAREFULL NEEDS MANUAL NUMBER
  description: ''
  triggers:
  - trigger: time_pattern
    minutes: /2
  conditions: []
  actions:
  - action: modbus.write_register
    metadata: {}
    data:
      hub: SAX_Battery_A
      address: 41
      slave: 64
      value: '[{{(-7000| float(0))|int|bitwise_and(0xffff)}}]'
    enabled: true
  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)}}]'
        enabled: true
      - repeat:
          sequence:
          - delay:
              hours: 0
              minutes: 15
              seconds: 0
          - action: modbus.write_register
            metadata: {}
            data:
              slave: 64
              address: 41
              hub: SAX_Battery_A
              value: '[{{(0-(states.input_number.sax_charge.state)|float(0))|int|bitwise_and(0xffff)}}]'
          while:
          - condition: state
            entity_id: input_boolean.full_charge
            state: 'on'
            for:
              hours: 0
              minutes: 0
              seconds: 0
    - 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
- id: '1738316113977'
  alias: 'rerun max power SAX '
  description: ''
  triggers:
  - trigger: time_pattern
    minutes: /2
  conditions:
  - condition: state
    entity_id: input_boolean.sax_full_power_charge
    state: 'on'
  actions:
  - 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)}}]'
  mode: single
- id: '1738342017156'
  alias: Manually set SAX to zero
  description: ''
  triggers:
  - trigger: event
    event_type: ''
  conditions: []
  actions:
  - action: modbus.write_register
    metadata: {}
    data:
      hub: SAX_Battery_A
      address: 41
      slave: 64
      value:
      - 0
      - 0
    enabled: true
  mode: single

For those following, I’m working on a custom components, soon available in HASS, that you can still install manually.
Work in pogress:
https://github.com/matfroh/sax_battery_ha/

small update on 2 of the sensors:
Moving from total to total_increasing enables the usage in the Energy Dashboard.
Where I’m still wondering is if I then need to move the scan_interval: 120 to an hourly frequency.

    - name: SAX_a_energy_produced
      unique_id: "uid-sax-a-energy_produced"
      unit_of_measurement: "Wh"
      device_class: energy
      state_class: total_increasing
      slave: 40
      address: 40096
      scan_interval: 3600
      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_increasing
      slave: 40
      address: 40097
      scan_interval: 3600
      data_type: uint16

Thanks for all the effort put into this! I tried downloading via HACS, but get the following Error when trying to Download:

<Integration matfroh/sax_battery_ha> Failed to download https://github.com/matfroh/sax_battery_ha/releases/download/tags/v0.1.3.2/sax_battery_ha-main.zip

Downloading IT manually I need to lookup Here:

https://github.com/matfroh/sax_battery_ha/archive/refs/tags/v0.1.3.2.tar.gz

Thanks for that! I added the right file to the release! Apologies!

Thanks. This worked, the files were downloaded to /custom_component/sax_battery/sax_battery_ha-main. Then it requests a restart, but after there is no Integration I can choose. Adding the files manually to sax_battery_ha without a subfolder in custom_components works.

In entities I see a SAX Power Home battery management update, but I get an endless spinner clicking IT.

Thanks for the Update. HACS has not yet added the extension to the store, apparently that takes month. Strange that it puts it in an additional folder. Will try to understand.

In any case, the right folder is <custom_components/sax_battery>, so that might be the error.

Once files are added in the right folder, after restart, you still need to manually go to devices and services, now use the store button or “+” at the lower end and search for “SAX” which should show you the extension and start the configuration screen. Let me know if that works, alternatively look in the log to see if something appears there

I suppose it takes long to add it to the store, but it was not available for integration. Even in the <custom_components/sax_battery_ha> generated I found again <custom_components/sax_battery>. So I had to move all files to the top directory and rename <sax_battery_ha> to <sax_battery>. Then I could add integration and it finds my battery. Maybe this helps to correct the scripts.

I believe I found the reason.
The zip file creates the subfolder. Will try to change this.

1 Like

So my hypothesis seemed correct. I have removed the zip file process.
You still need to download manually as the components is not in the store, but make sure the files are always in the folder

/custom_components/sax_battery/

nothing more, nothing less.
By the way, I have corrected this thing and released a new version which should make the slider work if your register 43 and 44 are writable and addressed a minor sensor warning.

Thanks again for pointing me to the right place.

Thanks for the update. It was automatically detected after a HACS reload and after restart, everything works like a charm. I can read max charge and discharge power, which was not possible before.

One minor thing: Is there a way to release the slider after the value was ready? Otherwise the tooltip ob the slider stays until another button is used in the dashboard.

Can you please elaborate on “release the slider”, I’m not sure I understand what you mean.
Also, be careful as the slider does not read the value and sets it only. Check the German forum in my first post here or the issue I raised in GitHub which this fix addresses. To what I know, register 43 and 44 do not reply with the value, so my code now simply does not expect a reply and assumes it went through. Also, I have programmed to reset this value every 5 minutes, as the battery system keeps the value only for a given time that is not documented.

If you have the register 43 and 44 enabled and run the battery with the smart meter, I am keen to hear if it works as expected. I cannot do the test completely because I do not have the smart meter and I write register 41 and 42 to pilot the battery, which cancels register 43 and 44…

Keen to hear if it works for you!

When I use the slider on my mobile, I see a bubble indicating the value of the slider. This is never disappearing after I changed the value.

My setup is with smart meter and only 43 and 44 writable. When I change the value on the slider, I still see the old value on the right side of the slider after change. The slider sticks to the new value. If reading was possible, the value on the right side switches to the value of the slider. This might take up to 5 seconds. If reading returns the old value (error writing), the slider jumpa back to the old value. This is very convenient.

Thanks for the feedback. The issue is with SAX, that does not return a value. So nothing I can do for now on my end.
At some point I will gather multiple inputs and send it to their customer service to understand how this works on their end, as what we do here is reverse engineering and doing a lot of guessing, their documentation does not provide more visibility, so that’s why for now it’s best to force the value. The user interface thing seems to be a HA bug or behavior.

Would be great if you could report back on your testing. When you limit discharge, do you see it as well on your house meter? Also, are you using 1 or 2 or more batteries? Your help will be helpful.

I have one battery. But I just found out the battery is not reacting to my slider input, so it is not limiting the charge rate. Tomorrow I will have an update for my battery and I will ask suppert if the registers are configured correctly. It seems the battery work like delivered out of the box.

Then maybe try as well with the code posted at the top of this topic. If this works and my custom extension does not work, the error is in my component.

On changing the slider for charge rate I get this log entry, but I do not understand why register 45 is requested. If you are writing to 45 when changing 44?

Logger: custom_components.sax_battery
Quelle: custom_components/sax_battery/init.py:228
Integration: SAX battery pilot for HomeAssistant (Dokumentation, Probleme)
Erstmals aufgetreten: 05:22:15 (22 Vorkommnisse)
Zuletzt protokolliert: 11:08:13

Modbus error reading address 40097: Modbus Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 8 bytes (0 received)
Modbus error reading address 45: Modbus Error: [Input/Output] Modbus Error: [Invalid Message] No response received, expected at least 8 bytes (0 received)
Modbus error reading address 45: Modbus Error: [Input/Output] timed out
Modbus error reading address 45: Modbus Error: [Input/Output] No Response received from the remote slave/Unable to decode response
Modbus error reading address 45: Modbus Error: [Input/Output] [Errno 104] Connection reset by peer