Late last year I posted an article about integrating my SigEnergy SigenStor into Home Assistant using Modbus. That post was my first go at reading telemetry from my SigEnergy system via Modbus. The logic on when to charge, when to discharge and so on all stayed within the SigEnergy eco-system as my energy plan was basic at that stage, by all means it seemed the safer approach.
Fast forward a year I recently changed energy retailers and the SigEnergy app has become an issue given it lacks the flexibility I need.
I dont leverage a VPP (Virtual Power Plant) as I dont want to ideally duty cycle my battery more than 50% each day but with so much summer sun, high HVAC loads and variable weather in just a few months of spring its become clear I needed to almost daily adjust settings (discharge state of charge) in the SigEnergy app in order to get the best out my investment.
As this post is authored (October 2026) I am with AGL’s Battery Rewards which has has the following rates in Victoria, Australia
- Time of use tariff with a feed-in window between 5pm and 9pm that pays 25.5c/kWh.
- Outside of that window feed-in is 0.5c/kWh.
- Peak import between 3pm and 9pm costs 42.6c/kWh,
- Off peak import is 22c/kWh.
- Daily supply charge of $1.15c
Sny solar I export outside the high feed in window (5pm –> 9pm) is worth basicallynothing, but every kWh I can push out of the battery between 5pm and 9pm is worth 50x that. With a 40kWh battery, an array of 14.5kw and summer / day light savings I can export at the full 5kW grid export limit for the whole 4 hour window and in theory have plenty left to get through the night.
But that true most times, but not always the case due to a large whole house Fujitsu HVAC which can consume more than 40kw in a day(worst case).
What I am saying here is I need flexibility, far more than what SigEnergy’s apps can do.
This post is about Remote EMS and thats where Remote EMS comes in to play, but firstly what is an EMS?
An energy management system (EMS) is a system of computer-aided tools used by operators of electric utility grids to monitor, control, and optimize the performance of the generation or transmission system. Also, it can be used in small scale systems like microgrids.
https://en.wikipedia.org/wiki/Energy_management_system_(electrical_grid)
Home Assistant in this case will play the part of an EMS server over Modbus.
SigEnergy being incredibly flexible, call this control method Remote EMS.
So in this this post I am going to cover how to enable Remote EMS on a SigenStor and drive all configuration from Home Assistant, then once the core is explained, I will talk you through the value adds I have layered on top.
- Weather driven evening discharge
- Safety watchdog
- Winter mode.
This post assumes you have already followed the first post. This post details how to
- Enable Modbus TCP is on your SigenStor (installer access required)
- Install and configure Sigenergy ESS HACS integration is installed and reporting entities. If you can not telnet to your inverter on TCP 502, stop and go back to that post first.


What Is Remote EMS?
EMS stands for Energy Management System. Out of the box the SigenStor runs its own EMS, the logic in the Sigenergy app that decides when to charge, when to discharge and when to sit idle, based on the mode you picked in the app (self consumption, TOU and so on). Remote EMS is Sigenergy handing that decision making over to an external controller via Modbus. This is a double edged sword because you are now in control and battery needs to be told what to do.
In Modbus terms it is two holding registers on the plant (slave ID 247). Register 40029 enables or disables Remote EMS and register 40031 sets the control mode. The Sigenergy ESS integration exposes both as entities, a switch and a select, so you never have to touch a raw register, but it helps to know what is underneath when you are debugging. It is actually really simple once you get your head over this, you are in effect changing this register value.
| Mode | Register value | What it actually does |
|---|---|---|
| PCS Remote Control | 0 | Power on default. Battery idles and waits for power set points. House imports from the grid. |
| Standby | 1 | Battery idles. House imports from the grid. |
| Maximum Self Consumption | 2 | Normal behaviour. Solar to house, surplus to battery, battery to house at night. |
| Command Charging (Grid First) | 3 | Charge the battery from the grid. I never use this. |
| Command Charging (PV First) | 4 | Charge the battery from PV, top up from grid. |
| Command Discharging (PV First) | 5 | Export at the grid limit. PV serves the house and the export first, the battery fills the gap. This is my 5pm to 9pm mode. |
| Command Discharging (ESS First) | 6 | Export from the battery at the max discharging limit and curtail PV to make room for it. Only safe after dark, see the gotcha further down. |
The important bit, and the thing that caught me out, is that the first two modes make the battery do nothing. The first time I enabled Remote EMS in the Sigenergy app, before Home Assistant was able to write to the plant, the mode landed on PCS Remote Control and the house started importing from the grid with a 98% battery sitting right there in the garage. Remote EMS means remote, if nothing is driving it, nothing happens. Keep that in mind, I come back to it in the watchdog automation.
Step 1 – Enabling Remote EMS On SigEnergy
In the SigEnergy app you will need to change the Current Mode to ‘Remote EMS’.
I actually did this first and and realised that in doing this, because Home Assistant was not setup it started importing from the grid, whilst battery still had a high SOC. Just note you will be importing until you complete the following steps which should take 5 minutes. If you do not want to import, this step could be performed last .


Step 2 – Turn Off Read-Only Mode In The Integration
The Sigenergy ESS integration ships with a Read-Only Mode option and it defaults to on. In read-only mode every Modbus write is silently dropped. I spent a lot of time troubleshooting why configuration would not stick before I found the checkbox.
- In Home Assistant go to Settings > Devices & Services > Sigenergy ESS, then click the cog on the Sigen Plant entry.
- Select the plant (not the inverter) and hit Submit.
- Untick Read-Only Mode and submit. Host, port and slave ID stay as they were.
The integration reloads itself and from that point on writes make it to the plant.



Step 3 0 Enable The Control Entities
The integration creates the control entities but leaves most of them disabled (there are close to 300 entities). You need to enable two entities.
Go to Settings > Devices & Services > Entities and search for sigen_plant_remote_ems. You want these two:
switch.sigen_plant_remote_ems_controled_by_home_assistant– register 40029, the Remote EMS on/off switch (yes, controled is spelt that way in the integration)select.sigen_plant_remote_ems_control_mode– register 40031, the mode select from the table above
Open each one, click the cog and turn on Enabled.


While you are in there, enable these as well. I use all of them in the value adds further down.
number.sigen_plant_ess_max_discharging_limit– cap on battery discharge power in kW, reads 4294967.295 when unlimitednumber.sigen_plant_ess_max_charging_limit– same for chargingnumber.sigen_plant_ess_backup_state_of_charge– the reserve the plant keeps for a blackoutnumber.sigen_plant_ess_charge_cut_off_state_of_charge– register 40047, the SoC the plant stops charging at. Read the gotcha section before you skip this onenumber.sigen_plant_ess_discharge_cut_off_state_of_charge– the SoC the plant stops discharging at
A word of warning on the entity search. If you filter on cut to find the cut-off entities and bulk enable, you will also pick up two grid code frequency numbers you really do not want to touch. Enable by exact name. Also ignore switch.sigen_inverter_remote_ems_dispatch if you see it, that register (41500) was removed in Modbus protocol v2.8 and the entity is a dead leftover.
Step 4 – Taking Control
Now the fun part. Turn on the Remote EMS switch. The select only becomes available while the switch is on, before that it sits at unavailable. Within one poll (5 seconds for me) the mode select will populate with whatever the plant is currently in, which on first enable will be PCS Remote Control. Change it to Maximum Self Consumption straight away and the battery goes back to behaving normally and it will stop your grid import


You can of course do this from Developer Tools > Actions, which is handy for testing before you wire anything in to an automation.
# Developer Tools > Actions
action: select.select_option
target:
entity_id: select.sigen_plant_remote_ems_control_mode
data:
option: Command Discharging (PV First)
And to validate end to end, Developer Tools > Template gives you the state of play in one hit.
# Developer Tools > Template
Remote EMS: {{ states('switch.sigen_plant_remote_ems_controled_by_home_assistant') }}
Mode: {{ states('select.sigen_plant_remote_ems_control_mode') }}
Battery: {{ states('sensor.sigen_plant_battery_state_of_charge') }}%
Export: {{ states('sensor.sigen_plant_grid_export_power') }} kW
Import: {{ states('sensor.sigen_plant_grid_import_power') }} kW
Set the mode to Command Discharging (PV First) and watch sensor.sigen_plant_grid_export_power climb to your export limit (5kW in my case) within a few seconds. If the sun is still up keep an eye on sensor.sigen_plant_pv_power too, it should keep generating and the battery only makes up the difference. Set it back to Maximum Self Consumption and export drops to zero. That is the whole mechanism. Everything from here on is just Home Assistant deciding when to flip between those two modes.
One more thing, whilst obvious. If you turn the Remote EMS switch off, control goes back to the Sigenergy app and whatever mode you had configured there. If things do go rogue or perhaps Home Assistant is down for an extended period of time, this will always be an option.
Value Add 1 – A Force Discharge Toggle Instead Of A Mode Select
Depending on your situation, I would prefer my battery to run flat and import, than accidently importing from the grid. So the first thing I did was abstract importing behind a single input boolean, Battery Force Discharge. On means Command Discharging (PV First), off means Maximum Self Consumption. Nothing else is reachable.
In this code, a toggle for force discharge and I am using input number for how low the battery is allowed to go, and a winter mode toggle I will get to below.
# configuration.yaml (or create these via Settings > Devices & Services > Helpers)
input_boolean:
battery_force_discharge:
name: Battery Force Discharge
icon: mdi:battery-arrow-down
battery_winter_mode:
name: Battery Winter Mode
icon: mdi:snowflake
input_number:
battery_discharge_floor:
name: Battery Discharge Floor
min: 0
max: 100
step: 0.5
initial: 62.5
mode: box
unit_of_measurement: "%"
icon: mdi:battery-low


Value Add 2 – Mode Sync Watchdog
This automation is the glue between the toggle and the plant and it is the most important one in the set. It does two jobs.
First, whenever the toggle changes it writes the matching mode to the select.
Second, it re-evaluates every 5 minutes and any time the select changes on its own, so if the plant ever ends up in Standby or PCS Remote Control (power cycle, firmware update, me fat fingering the app) it is dragged back to the correct mode within 5 minutes.
A couple of details worth calling out. The automation only acts while the Remote EMS switch is on, so turning the switch off really does hand control back to the app. As I have stopped the ability to import, there has been a run of 3 days where there has been no sun in Victoria. In these cases if I want to purchase power at off peak, I need to change the mode off Remote EMS and this means its very intentional.
The wait template is there because the select goes briefly unavailable while the integration polls, and writing to an unavailable select throws an error. And it only calls select.select_option when the mode actually needs to change, no point hammering Modbus every 5 minutes for nothing.
alias: "Sigen : Remote EMS mode sync (force discharge / self consumption)"
description: >-
Keeps the Sigenergy Remote EMS control mode in step with the Battery Force
Discharge toggle. On = Command Discharging (PV First) (PV serves house and
export first, battery fills the gap to the 5 kW export cap; ESS First
curtailed PV to 0 while the sun was up), Off = Maximum Self Consumption.
Also acts as a watchdog so the mode is never left in Standby or PCS Remote
Control (which make the house import from the grid).
triggers:
- trigger: state
entity_id: input_boolean.battery_force_discharge
- trigger: state
entity_id: switch.sigen_plant_remote_ems_controled_by_home_assistant
to: "on"
- trigger: state
entity_id: select.sigen_plant_remote_ems_control_mode
- trigger: time_pattern
minutes: "/5"
conditions:
- condition: state
entity_id: switch.sigen_plant_remote_ems_controled_by_home_assistant
state: "on"
actions:
# The select goes briefly unavailable while the integration polls, so wait for it
- wait_template: >-
{{ states('select.sigen_plant_remote_ems_control_mode') not in ['unavailable','unknown'] }}
timeout: "00:00:20"
continue_on_timeout: false
- variables:
target: >-
{{ 'Command Discharging (PV First)'
if is_state('input_boolean.battery_force_discharge','on')
else 'Maximum Self Consumption' }}
# Only write to Modbus when the mode actually needs to change
- condition: template
value_template: "{{ not is_state('select.sigen_plant_remote_ems_control_mode', target) }}"
- action: select.select_option
target:
entity_id: select.sigen_plant_remote_ems_control_mode
data:
option: "{{ target }}"
mode: queued
max: 3


Value Add 3 – Weather Driven Evening Discharge
This is basically the crux of this post and the why I went down this path. At 5pm, the start of the 25.5c feed-in window, this automation decides how much of the battery to sell. I was almost daily managing the battery SOC. I know we are talking a few dollars here but I find this fun in many ways, a form of data modelling.
Export too much and I am importing at 22c at 3am, which wipes out the gain. Export too little and I have 40kWh of battery sitting at 90% when the sun comes up and the panels get curtailed.
My floor depends on tomorrow. I pull the daily forecast from the Bureau of Meteorology integration (weather.lyndhurst in my case) and tier it. The BoM daily forecast only gives a condition and a max temperature, no cloud percentage, so the tiers are deliberately simple:
- Tomorrow sunny and max 23C or less – mild day, HVAC will not be working hard, the battery will refill easily. Discharge to 52.5%.
- Anything else – discharge to 62.5%.
- Tomorrow max under 15C, no forecast available, or Winter Mode on – no discharge at all, floor set to 100%. A cold day means heating overnight and poor generation tomorrow, keep every kWh.
It writes the floor into the input number, logs the decision and the reason to the logbook (I want to be able to see why it did what it did when I look the next morning), then turns the force discharge toggle on if the battery is above the floor. The mode sync automation takes it from there.
alias: "Sigen : Evening discharge start (5pm)"
description: >-
At 5pm works out the SoC floor from tomorrow's BoM forecast and stores it in
Battery Discharge Floor, then turns on Battery Force Discharge if the battery
is above the floor. Tiers: sunny and max <= 23C -> 52.5%; otherwise -> 62.5%;
max < 15C or Battery Winter Mode on -> no discharge (floor 100%).
triggers:
- trigger: time
at: "17:00:00"
conditions: []
actions:
- action: weather.get_forecasts
target:
entity_id: weather.lyndhurst
data:
type: daily
response_variable: fc
- variables:
tomorrow_date: "{{ (now() + timedelta(days=1)).strftime('%Y-%m-%d') }}"
tomorrow: >-
{{ (fc['weather.lyndhurst'].forecast | selectattr('datetime','search', tomorrow_date) | list | first)
if (fc['weather.lyndhurst'].forecast | selectattr('datetime','search', tomorrow_date) | list | count) > 0
else (fc['weather.lyndhurst'].forecast[1] if (fc['weather.lyndhurst'].forecast | count) > 1 else {}) }}
tmax: "{{ tomorrow.temperature | default(none) }}"
cond: "{{ tomorrow.condition | default('unknown') }}"
winter: >-
{{ is_state('input_boolean.battery_winter_mode','on')
or (tmax is none)
or (tmax | float(99)) < 15 }}
sunny_and_mild: "{{ cond == 'sunny' and (tmax | float(99)) <= 23 }}"
floor: "{{ 100 if winter else (52.5 if sunny_and_mild else 62.5) }}"
reason: >-
{{ 'Winter Mode on' if is_state('input_boolean.battery_winter_mode','on')
else ('no forecast' if tmax is none
else ('cold tomorrow (<15C)' if (tmax | float(99)) < 15
else ('sunny and mild' if sunny_and_mild else 'default'))) }}
- action: input_number.set_value
target:
entity_id: input_number.battery_discharge_floor
data:
value: "{{ floor }}"
# Leave a breadcrumb in the logbook so I can see why it did what it did
- action: logbook.log
data:
name: Sigen evening discharge
entity_id: input_boolean.battery_force_discharge
message: >-
Tomorrow {{ cond }} max {{ tmax }}C -> {{ reason }}, floor {{ floor }}%,
SoC {{ states('sensor.sigen_plant_battery_state_of_charge') }}%{{ ' (no discharge)' if winter else '' }}
- condition: template
value_template: "{{ not winter }}"
- condition: numeric_state
entity_id: sensor.sigen_plant_battery_state_of_charge
above: input_number.battery_discharge_floor
- action: input_boolean.turn_on
target:
entity_id: input_boolean.battery_force_discharge
mode: single
If you are not in Australia, swap the weather entity and the condition strings for whatever your weather integration returns. The forecast lookup is a bit defensive because weather.get_forecasts does not guarantee that index 1 is tomorrow, so it searches by date first and falls back to the second entry.
Value Add 4 – Stopping At 9pm Or At The Floor
The stop automation is quite basic with two triggers, 9pm when the feed-in rate drops back to half a cent, or the battery state of charge crossing below the floor, whichever comes first. It just turns the toggle off and the watchdog puts the plant back into Maximum Self Consumption.
alias: "Sigen : Evening discharge stop (9pm or SoC floor)"
description: >-
Turns off Battery Force Discharge at 9pm, or earlier when SoC drops below the
Battery Discharge Floor. The mode sync automation then returns the system to
Maximum Self Consumption.
triggers:
- trigger: time
at: "21:00:00"
- trigger: numeric_state
entity_id: sensor.sigen_plant_battery_state_of_charge
below: input_number.battery_discharge_floor
conditions:
- condition: state
entity_id: input_boolean.battery_force_discharge
state: "on"
actions:
- action: input_boolean.turn_off
target:
entity_id: input_boolean.battery_force_discharge
mode: single
Value Add 5 – Winter Mode
The forecast handles the day to day, but in the middle of winter I do not want to export at all. I am thinking come winter when I pivot back to a plan, like GloBird Zero Hero this will just be a button. Rather than fiddle with thresholds every season there is a Winter Mode toggle on the dashboard. When it is on, the 5pm automation sets the floor to 100% and does nothing.
Value Add 6 – Dashboard
All of this ends up as a row of tile cards in my Lovelace dashboard next to the Battery SOC tile on the main view. Force Discharge (red when on), EMS Mode so I can see what the plant is actually doing, Winter Mode, Max Charge and Min Charge. The tiles are deliberately the same size as their neighbours, tap the Force Discharge tile to toggle it, tap Max Charge to type a value.
# Tile cards on my Overview dashboard (Main view)
- type: tile
entity: sensor.sigen_plant_battery_state_of_charge
name: Battery SOC
vertical: false
- type: tile
entity: input_boolean.battery_force_discharge
name: Force Discharge
color: red
tap_action:
action: toggle
icon_tap_action:
action: toggle
- type: tile
entity: select.sigen_plant_remote_ems_control_mode
name: EMS Mode
- type: tile
entity: input_boolean.battery_winter_mode
name: Winter Mode
color: light-blue
tap_action:
action: toggle
icon_tap_action:
action: toggle
- type: tile
entity: number.sigen_plant_ess_charge_cut_off_state_of_charge
name: Max Charge
color: green
icon: mdi:battery-arrow-up
- type: tile
entity: number.sigen_plant_ess_discharge_cut_off_state_of_charge
name: Min Charge
color: orange
icon: mdi:battery-arrow-down
The result is the tile layout pictured at the top of this post. The energy dashboard from the first post keeps doing its thing too, now with a sizeable chunk of battery discharge going to the grid each evening rather than just the house.
Summary
Remote EMS gives you full control of your SigenStor , its a little bit complex but once you get your head around it the mechanics are simple. If you really want to extract the most value and flexibility our of your investment then its the path you need to take.
I know have Home Assistant in control, no doubt there it tweaking to be made but I now have a somewhat weather aware evening export into a 25.5c feed-in and a some basic fiscal safety (watchdog).
If you have a SigenStor connected to Home Assistant, enable Remote EMS and let your event driven house drive your battery too.
Thanks
Shane Baldacchino