Hoymiles MS-A2 Battery and MQTT

Hello all, I want to share what I learned when I set up my the Hoymiles MS-A2 with the MQTT capabilities.

You need to upgrade the MS-A2 to FW V01.05.xx (release 06.06.2025) or later. App needs to be V2.1 or later.

Enable MQTT: App/Settings/MQTT Service/Enable → your settings
Video (german) explains it in detail.

The Auto discovery only provides two values, as the others do not contain unique IDs. Add this to your configuration.yaml to have proper entities. Change the “XXX” to the number of your MS-A2:

mqtt:
  sensor:
  - name: "State of Charge"
    unique_id: hoymiles_msa2_soc
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/quick/state"
    value_template: "{{ value_json.soc | float(1) }}"
    unit_of_measurement: "%"
    device_class: battery
    state_class: measurement
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

  - name: "Batterie Temperatur"
    unique_id: hoymiles_msa2_batterie_temperatur
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/device/state"
    value_template: "{{ value_json.bat_temp }}"
    unit_of_measurement: "°C"
    device_class: temperature
    state_class: measurement
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

  - name: "Charge Today"
    unique_id: hoymiles_msa2_charge_today
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/system/state"
    value_template: "{{ value_json.chg_e }}"
    unit_of_measurement: "Wh"
    device_class: energy
    state_class: total_increasing
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

  - name: "Discharge Today"
    unique_id: hoymiles_msa2_discharge_today
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/system/state"
    value_template: "{{ value_json.dchg_e }}"
    unit_of_measurement: "Wh"
    device_class: energy
    state_class: total_increasing
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

  - name: "Power from(+)/to(-) Battery"
    unique_id: hoymiles_msa2_power_to_from_battery
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/quick/state"
    value_template: "{{ value_json.bat_p }}"
    unit_of_measurement: "W"
    device_class: power
    state_class: measurement
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

  - name: "Battery State"
    unique_id: hoymiles_msa2_state
    state_topic: "homeassistant/sensor/MSA-2XXXXXXXXXXX/quick/state"
    value_template: "{{ value_json.bat_sts }}"
    device_class: enum
    options:
      - discharge
      - charge
      - standby
    device:
      name: "MSA-2XXXXXXXXXXX"
      identifiers: "2XXXXXXXXXXX"
      manufacturer: "Hoymiles"
      model: "MS-A2"

If you want to use MQTT to control the battery, change
homeassistant/select/MSA-XXXXXXXXXX/ems_mode/command from general to mqtt_ctrl and update homeassistant/number/MSA-XXXXXXXXXX/power_ctrl/set values every 59 seconds.

Update interval: minimum 0.2 seconds, no longer than 1 minute. It will automatically switch back to “inherent logic” after 1 minute. And within the minute, the value must be changed. It is sufficient to change the decimal place, e.g., 100 —> 100.1.

E.g. to prevent the Battery from discharging at night change the value from 0.0 to 0.1 every 55 seconds.

If you want to use the inherent logic change homeassistant/select/MSA-XXXXXXXXXX/ems_mode/command from mqtt_ctrl to general.

Documentation posted here, direct link. At least the numbering and the update intervals are not correct, may be updated in the future.

**note:**  
`supported_topics` : indicates the topics that the device supports publishing and subscribing.  
`quick_state` : Topics published by the device for quick updates of the device itself and system status, see Section 10.  
`device_state` : A topic published by the device for timed updates of the device's own state, see Section 11.  
`system_state` : The topic published by the device for the scheduled update of the system state, see Section 12.  
`switch_ctrl` : The topic that the device responds to for device on/off control, see Section 7.  Not supported for now
`ems_mode` : The topic that the device responds to for setting the EMS mode, see Section 8.  
`power_ctrl` : The topic that the device responds to for controlling power, see Section 9.  

11. Device real-time Data (Device Release)
*The device is released at intervals of 1 second, including the device and system.*


Second-level data (Device publishing)
*Devices are published at intervals of 5 minutes.*

**note:**  
`type` : equipment port type (`grid_on` : and so, `grid_off` : so, `inv` : inverter)  
`v` : Device port voltage (minimum unit: 0.1V)  
`i` : Device port current (minimum unit: 0.01A)  
`f` : Device port frequency (minimum unit: 0.01Hz)  
`p` : Active power of the device port (minimum unit: 0.1W)  
`q` : Device port reactive power (minimum unit: 0.1VAR)  
`ein` : Device port input power of the day (minimum unit: 1Wh)  
`eout` : Device port output power of the day (minimum unit: 1Wh)  
`etin` : Historical cumulative input power of the device port (minimum unit: 1Wh)  
`etout` : Historical cumulative output power of device ports (minimum unit: 1Wh)  
`bat_sts` : Device battery status (`standby` : on standby, `charge` : charging, `discharge` : discharging, `lock` : locked)  
`bat_v` : Device battery voltage (minimum unit: 0.01V)  
`bat_i` : Device battery current (minimum unit: 0.01A)  
`bat_p` : Device battery power (minimum unit: 0.1W)  
`bat_temp` : Device battery temperature (minimum unit: 0.1 ° C)  
`soc` : Remaining battery power of the device (minimum unit: 0.01%)  
`rssi` : Device signal value (minimum unit: 1dBm)  

System real-time data (Device release)
*Devices are released at intervals of 5 minutes. This topic is only available for host and standalone devices.*

**note:**  
`pv_p` : System photovoltaic power (minimum unit: 0.1W)  
`plug_p` : System socket power (minimum unit: 0.1W)  
`bat_p` : System battery power (minimum unit: 0.1W)  
`grid_p` : System grid power (minimum unit: 0.1W)  
`load_p` : System load power (minimum unit: 0.1W)  
`sp_p` : System smart socket power (minimum unit: 0.1W)  
`soc` : System battery power (minimum unit: 0.01%)  
`pv_e` : Photovoltaic power generation of the system on the day (minimum unit: 1Wh)  
`dchg_e` : Battery side discharge capacity of the system for the day (minimum unit: 1Wh)  
`chg_e` : System charge on the battery side for the day (minimum unit: 1Wh)  
`plug_out_e` : System output of grid-connected sockets on the day (minimum unit: 1Wh)  
`plug_in_e` : System grid-connected socket input power of the day (minimum unit: 1Wh)  
`ems_mode` : The current EMS mode of the system (`general`: power controlled by the device itself,`mqtt_ctrl`: Power controlled by mqtt commands) 

EMS Mode configuration (Device Release)
**note:**  
`options` : System EMS mode (` general` : default, when enabled, the device manages energy through inherent logic, `mqtt_ctrl` : When enabled, the device responds to mqtt instructions to control power, which can be managed through the `homeassistant/number/<dev_id>/power_ctrl/set` topic) 

Power Control (Device Subscription)
**note:**  
See `min` and `max` in the `homeassistant/number/<dev_id>/power_ctrl/config` theme. (-1000 and 2000)

5 Likes

First automation I did was to delay discharging at night to reach the lowest shortly SoC point shortly before charging again.

Pretty static in the moment, needs improvements, but a good example to show how the MQTT interface works.

Further Ideas:
Prevent discharge under 100W, as the efficiency is really low in that range.

alias: MSA discharge stop 60 %
description: Stop discharge at 60% and start discharging again 3 hours before sunrise and more than 4kWh production predicted
triggers:
  - type: battery_level
    device_id: f9a40a47a1dc82d0c957be7d16b22039 #Battery
    entity_id: 21838c99796f4deef16f70125d25ba35 #SoC
    domain: sensor
    trigger: device
    below: 60
conditions: []
actions:
  - device_id: f9a40a47a1dc82d0c957be7d16b22039 #Battery
    domain: select
    entity_id: ee9494d8ad59ea190d8753bc5f021c86 #..._mqtt_select
    type: select_option
    option: mqtt_ctrl
  - repeat:
      while:
        - condition: template
          value_template: |
            {% set sunrise = state_attr('sun.sun', 'next_rising') %} 
            {% if (as_timestamp(sunrise) - as_timestamp(now())) > (3 * 60 * 60) 
               and 
               states('sensor.energy_production_today') | float > 4 %} 
              true
            {% else %}
              false
            {% endif %}
      sequence:
        - device_id: f9a40a47a1dc82d0c957be7d16b22039 #Battery
          domain: number
          entity_id: 92433e10be5767d336de1e2060cebdb6 #Power control set
          type: set_value
          value: 0
        - delay: "00:00:55" #change value every minute
        - device_id: f9a40a47a1dc82d0c957be7d16b22039 #Battery
          domain: number
          entity_id: 92433e10be5767d336de1e2060cebdb6 #Power control set
          type: set_value
          value: 0.1
        - delay: "00:00:55" #change value every minute
mode: single

Worked quite well, battery was discharged to 11% and then the solar energy production was high enough to charge the battery again. The green rectangle highlights the time the automation is active:

2 Likes

If you have two or more MS-A2 in a master/slave config you can address the system as well as the individual state:

…
state_topic: "homeassistant/sensor/MSA-2xxNUMMER_MASTER/quick/state"
value_template: "{{ value_json.sys_soc }}"
…
state_topic: "homeassistant/sensor/MSA-2xxNUMMER_MASTER/quick/state"
value_template: "{{ value_json.soc }}"

state_topic: "homeassistant/sensor/MSA-2xxNUMMER_SLAVE/quick/state"
value_template: "{{ value_json.soc }}"
2 Likes

thank you @skipper22hassio - I think about an automation that not use the standard way between MS-A2 and Shelly 3em, so HA could control. You could ignore a specific Phase, for example.

Exactly. I’m also trying to automate one phase, but something’s not working.

1 Like

Same in my case - my automation is jumping to much. Maybe someone has a good idea

I also had the problem of oscillating power of the MS-A2. I added a moving average filter with 8s window to both, the domestic power meter and the “quick.state.grid_on_p” sensor.
Maybe a shorter window is possible, but the reaction is realy quick already.

In my setup, the Hoymiles MS-A2 is just plugged in with “Grid On”; no inverted attached directly.
I created a template sensor to control the power target of the MS-A2.
“grid_power_for_ms_a2” is the domestic meter’s power reading (via Tasmota Hichi IR) filtered with a moving average filter with 8s window width.
“ms_a2_grid_on_power_filtered” is homeassistant/sensor/MSA-xxx/quick/state also filtered with moving average 8s.
The various input_number entities are named self-explaing, I hope.

Template sensor for the MS-A2 power target.

{# negative is charging, positive is discharging #}
{# lower then +-9 is pratically off #}
{% set grid_power = states('sensor.grid_power_for_ms_a2')|float %}
{% set msa2_power = states('sensor.ms_a2_grid_on_power_filtered')|float %}
{% set chargeLim = -(states('input_number.ms_a2_charge_power_max')|float) %}
{% set dischargeLim = states('input_number.ms_a2_discharge_power_max')|float %}


  
{% set pwrSet = grid_power+msa2_power-10 %}

{% if (pwrSet < -10) and (states('sensor.ms_a2_soc')|float > 95) %}
  {# stop further charging #}
  {% set pwrSet = 0 %}
{% endif %}

{# add deadzone with on/off hysterisis and saturate to limits #}
{% if is_state('sensor.ms_a2_state','standby') and (pwrSet > -50) and (pwrSet < 50) %}
  {% set pwrSet = 0 %}
{% elif (pwrSet > -30) and (pwrSet < 30) %}
   {% set pwrSet = 0 %}
{% elif pwrSet < chargeLim %}
  {% set pwrSet = chargeLim %}
{% elif pwrSet > dischargeLim %}
  {% set pwrSet = dischargeLim %}
{% endif %}

{{ pwrSet }}

This sensor triggers this automation:

alias: MS-A2 power control
description: ""
triggers:
  - trigger: state
    entity_id:
      - sensor.ms_a2_power_ctrl_setpoint
  - trigger: time_pattern
    seconds: /30
conditions: []
actions:
  - choose:
      - conditions:
          - type: is_charging
            <WARP2 wallbox is in charging state>
        sequence:
          - variables:
              powerSetpoint: |
                {{ 0.0 }}
        alias: When WARP2 charging disable MS-A2
      - conditions:
          - condition: numeric_state
            entity_id: sensor.ms_a2_power_ctrl_setpoint
            below: -5
          - condition: state
            entity_id: sensor.ms_a2_state
            state: standby
          - type: is_battery_level
            condition: device
            device_id: 5a22c817d46998b2a6449b5e9039bb62
            entity_id: f11b09d6138591be7e1ea921fbc406de
            domain: sensor
            above: 92
        sequence:
          - variables:
              powerSetpoint: >
                  {{ 0.0 }}
        alias: When in standby and high SoC, no charging
      - conditions:
          - condition: numeric_state
            entity_id: sensor.ms_a2_power_ctrl_setpoint
            above: 5
          - condition: numeric_state
            entity_id: sensor.current_electricity_market_price_cent
            below: input_number.ms_a2_discharge_tariff_threshold
        sequence:
          - variables:
              powerSetpoint: |
                {{ 0.0 }}
        alias: When market price is low, no discharging
    default:
      - variables:
          powerSetpoint: |
            {{ states('sensor.ms_a2_power_ctrl_setpoint') }}
  - variables:
      powerSetpoint: >
        {# minor random offset to make MS-A2 update to actually constant
        setpoint#} {% if powerSetpoint < 0 %}
          {% set powerSetpoint = powerSetpoint + (states('sensor.ms_a2_power_ctrl_random_offset')|float)/10.0 %}
        {% else %}
          {% set powerSetpoint = powerSetpoint - (states('sensor.ms_a2_power_ctrl_random_offset')|float)/10.0 %}
        {% endif %} {{ powerSetpoint }}
  - device_id: <MS-A2 device>
    domain: select
    entity_id: EMS mode
    type: select_option
    option: mqtt_ctrl
  - action: mqtt.publish
    metadata: {}
    data:
      topic: homeassistant/number/MSA-xxxxx/power_ctrl/set
      payload: "{{ powerSetpoint }}"
mode: single

“sensor.ms_a2_power_ctrl_random_offset” is a random number helper with values 0-50. This results in a minor variation of the setpoint and makes the MS-A2 keep listening to the mqtt commands.

Next step could be to add a cheap-grid charging case.

Works quite good. The oscillating grid power 11:00 to 13:00 is the wasching machine’s motor.

4 Likes

May I ask how the update frequency of your origin sensors is? I set up your solution for me, but e.g my Tasmota Hichi IR is sending new values every 10 seconds, while MS-A2 sends new values every second.

Furthermore, unfortunately, my values are oscillating pretty heavy:


Any idea why this is the case?

Hi guys,

I have a MS-A2 hoymiles battery and works fine in autoconsommation mode with Shelly Pro 3 EM.
But I wonder if it is possible to control it from Home Assistant and MQTT to configure the battery in standby during nights and wake-up during days ?
Thanks in advance

1 Like

Great help with getting my first battery up and running on Home Assistant. I have a second battery due today and I’m clear enough about adding it as a slave to the first in the Hoymiles app, but what do I need to do to add the second to Home Assistant? I should say at th emoment, I am just using Home Assistant for data gathering and monitoring, not control (although that may be a next step). Thanks again for an excellent thread.

With MQTT Control, you need to send different power demand values at least every 1 min. If you don’t, the MS-A2 falls back to internal control.
Maybe you could use this behavior and send very small power setpoints only at night (below 30 W results in standby). And/or use the ems_mode topic.

See here.
Looks like you just get further topics.
But I am confident that it’s sufficient to send power demand values to the master, because the autodiscovery mqtt number is configured with limits -2000 to +2000 Watts, which only makes sense for 2 devices.

Update: according to docu, the power control is limited -1000 to 1000. But it also says, that this topic only applies to the master device. So I bet you can send higher values, if you increase limits in the S-miles app once.

1 Like

Interested in your throw away line of “…two OR MORE…” I would dearly love to have another MS-A2 to take capacity up to around 6.75kWh and it seems that the system might support that, with veiled statements from some ‘experts’. Have you any direct experience?