Deye + Home Assistant Automations: Battery, TOU, Tariffs & PV Forecast
Mark
Autor
Deye + Home Assistant Automations: Battery, TOU, Tariffs & PV Forecast

Did you like this article?

Creating guides takes a lot of time, and I don’t want ads on the site. If I’ve helped you and you feel like it — buy me a coffee. It really motivates me to keep writing!

Automating Deye in Home Assistant: battery, Time of Use, tariffs, PV forecast and surplus power, step by step

Just displaying production, home consumption and battery state is only the starting point. The real value of a Deye integration in Home Assistant shows up once the system starts making predictable decisions: protecting the battery reserve, charging the storage in a cheap window, leaving room for solar production, building up a buffer before an outage, and only switching on big loads once there is real export.

This guide starts from the basics. It first explains Time of Use and the entities it needs. Then we build automations, starting with the simplest alerts and manual profiles and moving on to tariffs, PV forecasting, dynamic prices, surplus power, heat pumps and EV charging. Every automation has a manual override, a data-freshness check, a limit on how often it writes, a test procedure, and a safe fallback state.

Most important warning: the write entity names depend on your Deye model, your integration’s profile, and how the inverter is connected. The examples use descriptive placeholder names. Swap them for the entities you actually see in your installation. Never copy register numbers from a different inverter family.

Deye and Home Assistant series — where are you in it?

This article is the third part of a complete series. It assumes the inverter is already correctly connected and the key values have already been verified. If you’re just starting out, or your data looks suspicious, work through the earlier parts first.

The golden rule: one system decides, one system writes

The most dangerous setup is the one where several independent mechanisms change the same settings. The inverter’s own schedule, a Home Assistant automation, SolarAssistant, Node-RED, EMHASS and the manufacturer’s app can each work fine on their own, but together they’ll start overwriting each other’s SOC, time windows and Grid Charge settings.

Don’t create multiple owners of control: pick a single execution layer. Every other system can supply data and recommendations, but none of them should be writing the same parameters at the same time.
Data layer: Deye, the energy meter, the PV forecast, prices and loads.
Decision layer: automations, or EMHASS, computing the target profile.
Execution layer: a single script that writes the settings to the inverter.
Supervision layer: a watchdog, read-back verification, notifications and a kill switch.
15+
scenarios and patterns
TOU
Time of Use
PV
forecast and export

Where do you want to start?

You don’t have to roll out the whole system at once. Pick the problem you want to solve first. The safest order is: check your entities, simulate the decisions, build one simple automation, and only then add the rest.

Will these automations work with your integration method?

Automating a boiler or a charger only needs correct sensors from the inverter. Changing SOC, Grid Charge and Time of Use also needs write-capable entities. That’s the single most important distinction in this whole article.

Method Monitoring SOC / TOU PV surplus
Solarman, sensors only Yes No Yes
Solarman with R/W entities Yes Depends on the profile Yes
Kellerza Sunsynk/Deye Yes For supported models Yes
ESPHome Yes Only if your config exposes writes Yes
MQTT Yes Depends on the bridge Yes
SolarAssistant Yes For settings that are exposed Yes
How can you tell? If the only entities you see for your Deye device are sensor ones, you have monitoring only. number, switch and select entities can let you change settings, but you need to check each one by hand before relying on it.

1. How Time of Use works on Deye

In Deye’s manuals, Time of Use is used to program when the grid or a generator is allowed to charge the battery, and when the battery is allowed to power your loads. A typical screen holds six programs. Each one covers a time, an SOC, a power limit, and permission for Grid Charge. What the fields actually mean can differ between inverter families and firmware versions, so the manual for your exact model always wins.

The simple mental model: Home Assistant doesn’t control the inverter’s power stage directly. It changes settings that Deye’s own firmware then acts on, within whatever limits the BMS allows.
Program time
Start of the next slot
Getting the order wrong can shift which slot is active.
SOC
Charging target or protection threshold
Confirm the exact meaning on your specific model’s screen.
Power
Power limit for the slot
Doesn’t override BMS or installation limits.
Grid Charge
Permission to charge from the grid
Leaving it on can rack up cost.
TOU enable
Turns the schedule on
Changing fields while TOU is disabled may do nothing at all.

2. Where Deye’s entities come from, and why they’re not the same for everyone

Examples in this article use names like sensor.deye_battery_soc, sensor.deye_grid_power or number.deye_minimum_soc. Home Assistant doesn’t invent these on its own. Entities only appear once the inverter has been connected to Home Assistant through a specific integration. It’s that integration that reads the inverter’s registers, names them, and exposes them as sensors, switches, dropdowns or number fields.

Connect the inverter first: if your Deye isn’t visible in Home Assistant yet, start with the first part of the series: How to connect a Deye inverter to Home Assistant — Solarman, RS485, ESPHome, MQTT and SolarAssistant. Only come back to the automations in this article once that integration is done.

The whole chain looks like this

1
Deye inverter
Its registers hold things like SOC, PV power, grid power, temperatures, the TOU schedule and various limits.
2
Communication path
A Solarman logger, direct RS485, an Ethernet gateway, ESPHome, MQTT or SolarAssistant reads that data.
3
HA integration
Translates the inverter’s data into objects Home Assistant understands.
4
Entities
Show up in Home Assistant as sensor, binary_sensor, switch, number, select or time.

Why the names differ between installations

  • A different integration may name the exact same value differently, e.g. sensor.deye_battery_soc, sensor.sunsynk_battery_soc or sensor.solar_assistant_battery_state_of_charge.
  • A different inverter profile may expose more or fewer registers.
  • A different Deye model may have a different register map and different write capabilities.
  • You can rename an entity yourself in Home Assistant, so the entity ID doesn’t have to match the name shown in the documentation.
  • Not every method gives you write access. Read sensors may be available while the number, select or switch entities responsible for TOU simply don’t exist.

How to find your own entities, step by step

  1. Open Settings → Devices & services.
  2. Find the integration you used to add Deye and open its device page.
  3. Open the entity list. Search by name for: battery SOC, grid power, PV power, load power, TOU, program, grid charge, minimum SOC.
  4. Click the entity and copy its entity ID, e.g. sensor.deye_battery_soc. Don’t just copy the friendly name shown on the card.
  5. Go to Developer tools → States, paste the ID in, and check the current value, unit and attributes.
  6. Compare the reading against the inverter’s own screen. Only once that’s confirmed should you put the entity into the YAML from this article.
The example entities won’t magically appear just because you paste the code: every sensor.deye_battery_soc name in this guide is a placeholder. If your integration created sensor.sunsynk_battery_soc, that’s exactly the name you need to use in your automation.

Which entities come from the inverter, and which ones you create yourself

Entities from the Deye integration
sensor..., number..., switch..., select... related to the inverter. These appear once Deye is correctly connected.
User-created helpers
input_boolean..., input_number... and input_datetime.... We create these ourselves to set thresholds and enable the automations.
Helper sensors
binary_sensor.deye_dane_swieze and sensor.deye_eksport_do_sieci — we build these in this article from the integration’s own entities.

3. What needs to exist in Home Assistant first

Before building an automation, check whether your integration exposes both the data and the write entities. Monitoring alone doesn’t mean you can change the schedule.

In the examples that follow: entities starting with sensor.deye..., number.deye... and switch.deye... mark data created by your inverter integration. Entities starting with input_... are helpers we create ourselves.

Measurement data

  • Battery SOC matching the inverter’s own screen.
  • Grid power with a confirmed sign for import versus export.
  • PV power in a known unit.
  • Battery power with a recognisable direction.
  • Home load power for load monitoring.
  • Data freshness or communication availability.

Control entities

  • Enabling TOU or the correct operating mode.
  • Grid Charge for the program in use.
  • Program SOC or minimum SOC.
  • Program time, if it needs to change dynamically.
  • Power limit, once confirmed for your model.
  • A way back to normal settings.
No R/W entities? You can still control a boiler, heat pump or EV based on Deye’s data. Controlling the battery itself needs a confirmed integration and a write profile.

4. Before you copy any YAML — four concepts for beginners

Entity is a single value or control in Home Assistant, e.g. battery SOC or the Grid Charge switch. Helper is a virtual dial or switch you create yourself. A trigger starts an automation, a condition only lets it run in a specific situation, and an action carries out the change.

Where do you paste an automation? The easiest route: Settings → Automations & scenes → Create automation → New automation → three dots → Edit in YAML. Paste code that starts with alias:, without adding an automation:.
  1. First find your own entities and record them in the table at the end of the article.
  2. Paste the code into the editor and swap out every example name.
  3. Save it, and only use “Run actions” once you understand what it does.
  4. After the test, open the automation’s Traces. You’ll see exactly which condition stopped it from running.

5. Helpers and a manual automation kill switch

Helpers let you change thresholds from the dashboard. The main switch has to stop every write, but it shouldn’t disable the inverter’s own local safeguards.

Where to paste this code

  1. In Home Assistant, open Settings → Devices & services → Helpers. For a beginner, the easiest path is creating helpers through the UI, without touching YAML.
  2. If you work from configuration files, paste the input_boolean, input_number and input_datetime blocks into configuration.yaml or your own package.
  3. After saving, use Developer tools → YAML → Check configuration, then restart Home Assistant.
  4. In Developer tools → States check that every entity starting with input_....
input_boolean:
  deye_automatyka:
    name: Deye — energy automation
    icon: mdi:home-lightning-bolt

  deye_tryb_awaryjny:
    name: Deye — emergency reserve
    icon: mdi:battery-alert

  deye_ladowanie_sieci:
    name: Deye — allow charging from grid
    icon: mdi:transmission-tower-import

input_number:
  deye_soc_normalny:
    name: Deye — normal SOC reserve
    min: 10
    max: 80
    step: 5
    unit_of_measurement: "%"
    icon: mdi:battery-medium

  deye_soc_awaryjny:
    name: Deye — emergency SOC reserve
    min: 30
    max: 100
    step: 5
    unit_of_measurement: "%"
    icon: mdi:battery-alert-variant

  deye_nadwyzka_start:
    name: Deye — export required to start
    min: 200
    max: 5000
    step: 100
    unit_of_measurement: "W"
    icon: mdi:transmission-tower-export

input_datetime:
  deye_tania_od:
    name: Deye — cheap tariff from
    has_date: false
    has_time: true

  deye_tania_do:
    name: Deye — cheap tariff until
    has_date: false
    has_time: true
Reference pattern: Home Assistant’s official input_number documentation. It shows the number helper being used as a value source and an automation trigger.
Rule of thumb: one switch stops Home Assistant’s automation, but the inverter stays on a safe, previously tested local profile.

The easiest route: create helpers in the UI

  1. Open Settings → Devices & services → Helpers.
  2. Click Create helper and choose Toggle. Name it “Deye — energy automation”.
  3. Create a Number helper for the normal and emergency SOC reserves. Set the unit to %, the step to 5 and a sensible range.
  4. Create Date and/or time helpers for the start and end of the cheap tariff.
  5. After creating each helper, open its settings and copy the entity ID. That’s the ID you’ll use in the automations.
For beginners, I’d recommend the UI. The YAML block with the helpers stays in the article as an option for anyone using packages or managing configuration through files.

6. Data freshness and power direction

An automation running on stale data can leave a charge or a load in the wrong state. Start by building a freshness sensor. In the example, data counts as fresh for 90 seconds after the last grid-power update.

First, work out the sign convention for grid power

  1. On a sunny day, while the house is exporting, note the value of the grid power sensor.
  2. Briefly switch on a big load so the house starts drawing power. Note the sign and value again.
  3. If import comes out positive and export negative, use the code below unchanged. If it’s the other way round, swap the import and export calculations.
  4. If your integration already exposes separate import and export sensors, use those instead of building templates.
template:
  - binary_sensor:
      - name: "Deye — basic data available"
        unique_id: deye_podstawowe_dane_dostepne
        availability: >
          {{ states('sensor.deye_grid_power') not in
             ['unknown', 'unavailable', 'none', ''] }}
        state: >
          {{ states('sensor.deye_grid_power') not in
             ['unknown', 'unavailable', 'none', ''] }}

  - sensor:
      - name: "Deye — export to grid"
        unique_id: deye_eksport_do_sieci
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        availability: >
          {{ states('sensor.deye_grid_power') not in
             ['unknown', 'unavailable', 'none', ''] }}
        state: >
          {% set grid = states('sensor.deye_grid_power') | float(0) %}
          {# This variant assumes: import positive, export negative. #}
          {{ [0, -grid] | max | round(0) }}

      - name: "Deye — import from grid"
        unique_id: deye_import_z_sieci
        unit_of_measurement: "W"
        device_class: power
        state_class: measurement
        availability: >
          {{ states('sensor.deye_grid_power') not in
             ['unknown', 'unavailable', 'none', ''] }}
        state: >
          {% set grid = states('sensor.deye_grid_power') | float(0) %}
          {{ [0, grid] | max | round(0) }}
Reference pattern: Home Assistant — common template patterns. The code follows the official rules for reading states and building safe template sensors.
An important limit of the availability sensor: this code confirms the entity has a valid state, but it doesn’t measure time since the last communication frame. If your integration exposes its own “last update”, “data age” or availability sensor, use that instead. last_updated alone isn’t always a reliable freshness marker if the device keeps reporting an identical value for a long stretch.
Check the sign: one integration reports import as positive, another reports export as positive. Switch on a big load and compare the sensor against the inverter’s own screen before relying on the template.

Simulate first: check the decisions without touching the inverter

This is the safest way to start. Instead of immediately calling number.set_value or or switch.turn_on

alias: Deye — SOC decision simulation
description: Doesn't change the inverter, only logs the planned decision
triggers:
  - trigger: time_pattern
    minutes: "/15"
conditions:
  - condition: state
    entity_id: input_boolean.deye_automatyka
    state: "on"
actions:
  - variables:
      nowy_soc: "{{ states('input_number.deye_soc_normalny') | int }}"
      obecny_soc: "{{ states('number.deye_minimum_soc') | int(-1) }}"
  - action: logbook.log
    data:
      name: "Deye — simulation"
      message: >
        Current limit: {{ obecny_soc }}%.
        Would set the limit to {{ nowy_soc }}%.
mode: single
Where do you see the result? Open the Logbook and search for “Deye — simulation”. If the messages match what you’d expect, you can move on to the version that actually writes.

7. Scenario 1 — manual battery reserve

Type of scenarioWrite to the inverter
Entities neededBattery SOC, the minimum-SOC entity, a number helper

The first automation does exactly one thing: it copies the helper’s value into the minimum-SOC entity. It’s the safest possible test of the whole write chain.

Preparation, step by step

  1. In Developer tools → States find the number entity that corresponds to minimum SOC or the battery reserve.
  2. Change it by hand by 5 points and check the inverter’s screen. If the change doesn’t come back to HA within a few seconds, don’t build the automation yet.
  3. In the code, replace number.deye_minimum_soc with your own entity.
  4. Create the automation in the UI, switch the editor to YAML and paste the code without the automation:.
alias: Deye — set normal battery reserve
id: deye_ustaw_normalna_rezerwe_baterii
description: Copies the helper's value into the minimum-SOC entity after checking the data.
triggers:
  - trigger: state
    entity_id: input_number.deye_soc_normalny
    for: "00:00:10"
conditions:
  - condition: state
    entity_id: input_boolean.deye_automatyka
    state: "on"
  - condition: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    state: "on"
actions:
  - variables:
      nowy_soc: "{{ states('input_number.deye_soc_normalny') | int(20) }}"
      obecny_soc: "{{ states('number.deye_minimum_soc') | int(-1) }}"
  - condition: template
    value_template: "{{ obecny_soc >= 0 and nowy_soc != obecny_soc }}"
  - action: number.set_value
    target:
      entity_id: number.deye_minimum_soc
    data:
      value: "{{ nowy_soc }}"
  - delay: "00:00:05"
  - if:
      - condition: template
        value_template: >
          {{ states('number.deye_minimum_soc') | int(-1) == nowy_soc }}
    then:
      - action: logbook.log
        data:
          name: Deye
          message: "Normal reserve confirmed: {{ nowy_soc }}%"
    else:
      - action: persistent_notification.create
        data:
          title: "Deye — SOC write not confirmed"
          message: >
            Tried to set {{ nowy_soc }}%, but the entity shows
            {{ states('number.deye_minimum_soc') }}%.
mode: restart
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: Kellerza Deye/Sunsynk — R/W sensor definitions. The project marks which settings are readable and writable; the exact entity name depends on your add-on’s configuration.

Test

  1. Turn the automation off and change the helper — the inverter shouldn’t change.
  2. Turn the automation on and change the value by 5 points.
  3. Compare the entity, the Deye screen and the manufacturer’s app.
  4. Restart HA; the same value shouldn’t get written again for no reason.
  5. When data is unavailable, the write must be blocked.
What should happen? After changing the helper, the minimum-SOC entity and the inverter’s screen should show the same value.

8. Scenario 2 — charging during a fixed cheap-tariff window

Type of scenarioWrite to the inverter
Entities neededSOC, Grid Charge, program SOC, two time helpers

The most predictable option is a fixed tariff window. The system checks the time, the user’s permission and the SOC. Only then does it turn on Grid Charge and set the target.

Preparation, step by step

  1. First set one TOU program manually, without Home Assistant, and watch how Deye interprets the time, SOC and Grid Charge.
  2. Find the switch entity responsible for Grid Charge on the relevant program, and the number entity for its SOC.
  3. Set the start and end helpers for the cheap tariff. The code also handles a window that crosses midnight, e.g. 22:00–06:00.
  4. For the first test, set the SOC target only a few points above the current one. Don’t start with charging to 100%.
alias: Deye — night charging in a fixed cheap tariff
id: deye_nocne_ladowanie_stala_taryfa
description: Turns on Grid Charge within the set window until the SOC target is reached.
triggers:
  - trigger: time_pattern
    minutes: "/5"
  - trigger: state
    entity_id:
      - input_boolean.deye_automatyka
      - input_boolean.deye_ladowanie_sieci
      - input_datetime.deye_tania_od
      - input_datetime.deye_tania_do
conditions: []
actions:
  - variables:
      dane_ok: >
        {{ is_state('binary_sensor.deye_podstawowe_dane_dostepne', 'on') }}
      automatyka_on: >
        {{ is_state('input_boolean.deye_automatyka', 'on') }}
      zgoda_on: >
        {{ is_state('input_boolean.deye_ladowanie_sieci', 'on') }}
      soc: "{{ states('sensor.deye_battery_soc') | float(-1) }}"
      cel_soc: 80
      start: "{{ states('input_datetime.deye_tania_od')[0:5] }}"
      stop: "{{ states('input_datetime.deye_tania_do')[0:5] }}"
      teraz: "{{ now().strftime('%H:%M') }}"
      w_oknie: >
        {% if start < stop %}
          {{ start <= teraz < stop }}
        {% else %}
          {{ teraz >= start or teraz < stop }}
        {% endif %}
      ma_ladowac: >
        {{ dane_ok and automatyka_on and zgoda_on
           and soc >= 0 and soc < cel_soc and w_oknie }}
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ ma_ladowac }}"
        sequence:
          - if:
              - condition: template
                value_template: >
                  {{ states('number.deye_program_1_soc') | int(-1) != cel_soc }}
            then:
              - action: number.set_value
                target:
                  entity_id: number.deye_program_1_soc
                data:
                  value: "{{ cel_soc }}"
          - if:
              - condition: state
                entity_id: switch.deye_program_1_grid_charge
                state: "off"
            then:
              - action: switch.turn_on
                target:
                  entity_id: switch.deye_program_1_grid_charge
    default:
      - if:
          - condition: state
            entity_id: switch.deye_program_1_grid_charge
            state: "on"
        then:
          - action: switch.turn_off
            target:
              entity_id: switch.deye_program_1_grid_charge
mode: single
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: Deye/Sunsynk — Inverter Automation. The Time-of-Use section shows working community automations that change charging modes and parameters; the MarkLabs code adds state checks and limits unnecessary writes.
Crossing midnight: a 23:00–06:00 window needs different logic than a 10:00–14:00 window. The example handles both cases.

When it makes sense

  • the price difference covers storage losses and fees,
  • mornings have high demand and the forecast is weak,
  • you need an emergency reserve,
  • your battery’s manufacturer allows charging from the grid.
A limit isn’t a guarantee: the BMS can reduce current because of temperature, cell voltage or SOC.
What should happen? During the cheap window, Grid Charge should turn on, and turn off once the target is reached or the window ends.

9. Scenario 3 — PV forecast and a dynamic SOC target

Type of scenarioWrite to the inverter
Entities neededtomorrow’s PV forecast, program SOC

Forecast.Solar can provide an energy forecast for tomorrow. A forecast is not a guarantee. React with wide bands and fall back to a neutral target when the sensor is unavailable.

≥20 kWh
Target 30%
Plenty of room for PV.
12–20 kWh
Target 50%
Partial reserve.
6–12 kWh
Target 70%
A bigger buffer.
<6 kWh
Target 90%
A weak day.
No data
Target 70%
A safe default profile.

Preparation, step by step

  1. Add the Forecast.Solar integration and check the actual name of tomorrow’s forecast sensor.
  2. Compare the forecast against actual production for at least a week. Note the typical error for sunny and overcast days.
  3. Adjust the 6, 12 and 20 kWh thresholds to your storage capacity and household consumption.
  4. The automation should only change the SOC target. Leave turning Grid Charge on to a separate, already-tested tariff automation.
alias: Deye — set charging target from PV forecast
id: deye_cel_ladowania_prognoza_pv
description: Once a day, picks the target SOC based on tomorrow's forecast production.
triggers:
  - trigger: time
    at: "21:30:00"
conditions:
  - condition: state
    entity_id: input_boolean.deye_automatyka
    state: "on"
  - condition: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    state: "on"
actions:
  - variables:
      prognoza: >
        {{ states('sensor.energy_production_tomorrow') | float(-1) }}
      cel_soc: >
        {% if prognoza < 0 %}
          70
        {% elif prognoza >= 20 %}
          30
        {% elif prognoza >= 12 %}
          50
        {% elif prognoza >= 6 %}
          70
        {% else %}
          90
        {% endif %}
  - condition: template
    value_template: >
      {{ states('number.deye_program_1_soc') | int(-1) != cel_soc | int }}
  - action: number.set_value
    target:
      entity_id: number.deye_program_1_soc
    data:
      value: "{{ cel_soc | int }}"
  - delay: "00:00:05"
  - action: logbook.log
    data:
      name: Deye
      message: >
        Tomorrow's PV forecast: {{ prognoza }} kWh.
        Overnight charging target set to: {{ cel_soc }}%.
mode: single
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: Home Assistant Community — Adaptive Solar Battery Charging. The thread describes a working system for Deye, with helpers, current limits, a forecast and multi-stage testing. The simplified example in this article keeps its core principle: the forecast sets the target rather than directly controlling every second of charging.
The thresholds are examples: tune them to your battery capacity and your consumption between the end of the cheap tariff and the start of production.
What should happen? In the evening, the automation picks an SOC target and logs the decision.

Full example: charging to 70% overnight ahead of a cloudy day

Now let’s put all the pieces together into one simple project. Say tomorrow’s forecast production is weak. During the cheap tariff, we want to top the battery up to 70% — but only when the Deye data is current.

  1. Check that the SOC in Home Assistant matches the Deye screen.
  2. Find the Grid Charge and target-SOC entities for the program you’re using.
  3. Create the start and end helpers for the cheap tariff.
  4. Run the simulation version for one evening and check the Logbook.
  5. Make one manual SOC target change of 5 points and confirm it on the inverter.
  6. Only then turn on the real tariff automation.
The first night is a test. Don’t leave the system unsupervised. Compare the time it turns on, the charging current, the SOC and the moment it turns off against what the inverter shows.
After a successful test, you can gradually replace the fixed 70% target with a value computed from the PV forecast. Don’t change the hours, the SOC, the power and several TOU programs all at the same time.

10. Scenario 4 — reserve before a grid outage

Type of scenarioWrite to the inverter
Entities neededminimum SOC, an emergency-mode helper

Trigger emergency mode manually at first. Only after testing should you link it to a weather warning or a planned outage. It raises the reserve and gives the tariff automation permission to charge.

Preparation, step by step

  1. At the start, only use the manual “emergency mode” helper.
  2. Set the normal and emergency reserves to values that are safe for your battery.
  3. Turn the mode on and check that minimum SOC rises, and that the separate tariff automation gets permission to charge.
  4. Turn the mode off and confirm it returns to the value from the normal-reserve helper. Only add weather or an operator alert later.
alias: Deye — manual emergency reserve mode
id: deye_reczny_tryb_rezerwy_awaryjnej
description: Raises minimum SOC when the helper turns on and restores the normal value when it turns off.
triggers:
  - trigger: state
    entity_id: input_boolean.deye_tryb_awaryjny
    to: "on"
    id: alarm_on
  - trigger: state
    entity_id: input_boolean.deye_tryb_awaryjny
    to: "off"
    id: alarm_off
conditions:
  - condition: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    state: "on"
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: alarm_on
          - condition: state
            entity_id: input_boolean.deye_automatyka
            state: "on"
        sequence:
          - action: number.set_value
            target:
              entity_id: number.deye_minimum_soc
            data:
              value: >
                {{ states('input_number.deye_soc_awaryjny') | int(60) }}
          - action: input_boolean.turn_on
            target:
              entity_id: input_boolean.deye_ladowanie_sieci
      - conditions:
          - condition: trigger
            id: alarm_off
        sequence:
          - action: number.set_value
            target:
              entity_id: number.deye_minimum_soc
            data:
              value: >
                {{ states('input_number.deye_soc_normalny') | int(20) }}
          - action: input_boolean.turn_off
            target:
              entity_id: input_boolean.deye_ladowanie_sieci
mode: restart
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: SolarAssistant — adjusting solar settings. The documentation recommends first blocking writes, testing that settings publish correctly, and only then enabling real writes — the same order used here.
Returning to normal: restore the helper’s value or a remembered state, not a random constant.

Tests

  • Trigger at a low SOC.
  • Trigger at an SOC higher than the target.
  • Turn it off mid-charge.
  • Restart HA with the helper active.
  • No data at the moment the alarm ends.
What should happen? After emergency mode turns on, the reserve rises; after it turns off, the normal value returns.

11. Scenario 5 — real PV surplus

Type of scenarioRead-only from Deye, controlling a load
Entities neededexport, SOC, a switch for a boiler or another load

The most common mistake is controlling things off PV power. 5 kW of production doesn’t mean 5 kW of surplus if the house and battery are using all of it. Use actual export instead.

Preparation, step by step

  1. Check the power draw of the load you’re controlling. The start threshold should be higher than that draw, with margin to spare.
  2. Set the minimum battery SOC above which surplus can go to a boiler or an EV.
  3. For a few days, run the automation in observation mode: instead of or log a message to the Logbook.
  4. After going live, check that turning off the main automation, or losing data, always turns the load off too.
alias: Deye — boiler from real PV surplus
id: deye_bojler_z_nadwyzki_pv
description: Turns the boiler on once export is stable and safely turns it off when the surplus or the data disappears.
triggers:
  - trigger: numeric_state
    entity_id: sensor.deye_eksport_do_sieci
    above: input_number.deye_nadwyzka_start
    for: "00:03:00"
    id: start
  - trigger: numeric_state
    entity_id: sensor.deye_eksport_do_sieci
    below: 200
    for: "00:02:00"
    id: stop
  - trigger: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    to: "off"
    id: brak_danych
  - trigger: state
    entity_id: input_boolean.deye_automatyka
    to: "off"
    id: automatyka_off
conditions: []
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: start
          - condition: state
            entity_id: input_boolean.deye_automatyka
            state: "on"
          - condition: state
            entity_id: binary_sensor.deye_podstawowe_dane_dostepne
            state: "on"
          - condition: numeric_state
            entity_id: sensor.deye_battery_soc
            above: 85
          - condition: state
            entity_id: switch.bojler_nadwyzka
            state: "off"
        sequence:
          - action: switch.turn_on
            target:
              entity_id: switch.bojler_nadwyzka
      - conditions:
          - condition: trigger
            id:
              - stop
              - brak_danych
              - automatyka_off
          - condition: state
            entity_id: switch.bojler_nadwyzka
            state: "on"
        sequence:
          - action: switch.turn_off
            target:
              entity_id: switch.bojler_nadwyzka
mode: restart
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: Home Assistant Community — PV Solar Excess Optimizer. This community project bases control of flexible loads on available surplus, thresholds and stability time. The code in this article is a deliberately simpler variant for a single load.

Hysteresis

Once a load switches on, export drops. An identical start and stop threshold causes rapid cycling. Turning on should require a higher threshold plus stability, and turning off a lower threshold held for a set time.

Compressors and EVs: add a minimum run time, a minimum rest time and a limit on how often they can switch.
What should happen? Once export is stable, the load turns on; once the surplus or the data disappears, it turns off.

12. Priorities: battery, hot water, heat pump and EV

Not every installation should trigger every scenario at once. Decide the order in which energy gets used.

<60% SOC
Battery
Don’t run big loads off the surplus.
60–85%
Battery + one flexible load
Some of the export can be used.
>85%
Self-consumption
A boiler or EV takes most of the surplus.
Emergency mode
Reserve
Flexible loads are locked out.
Priority is a personal choice: a household that charges an EV daily might treat the car as more important than a full battery. What matters most is that two automations don’t fight over the same energy.

13. Limiting import and shedding load

Home Assistant can disconnect non-critical loads, but it doesn’t replace electrical protection or the inverter’s own internal peak shaving.

Preparation, step by step

  1. Set a safe threshold together with an electrician, or based on your connection’s rated power and protection.
  2. Build a shedding order for your loads: boiler first, then the wallbox, other devices last.
  3. Test each step on its own. The code stops after turning off the boiler, so it doesn’t needlessly shed a second load in the same cycle.
  4. Build a separate automation that only restores the wallbox once import has stably dropped.
alias: Energy — load shedding on high import
id: energia_redukcja_obciazenia_wysoki_import
description: First turns off the boiler, then limits the wallbox.
triggers:
  - trigger: numeric_state
    entity_id: sensor.deye_import_z_sieci
    above: 7000
    for: "00:00:20"
conditions:
  - condition: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    state: "on"
actions:
  - if:
      - condition: state
        entity_id: switch.bojler_nadwyzka
        state: "on"
    then:
      - action: switch.turn_off
        target:
          entity_id: switch.bojler_nadwyzka
      - stop: "Boiler turned off — don't shed the wallbox in the same cycle."
  - if:
      - condition: numeric_state
        entity_id: number.wallbox_current
        above: 6
    then:
      - action: number.set_value
        target:
          entity_id: number.wallbox_current
        data:
          value: 6
mode: single
What you need to swap in the code: every entity starting with sensor.deye_, number.deye_, switch.deye_ and every controlled-load entity is a placeholder name. Copy your own IDs from Developer tools.
Reference pattern: Kellerza Deye/Sunsynk — Load Limit automation. The community documentation distinguishes behaviour for essential versus non-essential loads. The MarkLabs example doesn’t replace the inverter’s Load Limit — it sheds external loads instead.
The 7000 W threshold is an example: tune it to your phase count, connection power and protection.

14. EMHASS — the advanced level

EMHASS can factor in the PV forecast, load, buy and sell prices, battery parameters and shiftable loads. Its output is a power plan and an SOC trajectory. It doesn’t, however, automatically know how to control any given Deye.

  • EMHASS computes the plan.
  • The Deye integration exposes the execution entities.
  • The HA automation validates and limits the writes.
  • The inverter and BMS remain the safety layer.
When to roll out EMHASS: once your simple rules are already running reliably. Optimisation won’t fix a wrong power sign or unreliable entities.

15. Rollout plan and failure testing

Roll the system out in stages. Let each stage run for a few days before adding the next one.

  1. Monitoring and data freshness only.
  2. Manually changing a single SOC entity.
  3. A fixed tariff window.
  4. A target driven by the PV forecast.
  5. Manual emergency mode.
  6. Controlling a load from export.
  7. Dynamic prices or EMHASS.

Failure testing

  • Disconnect the internet, keeping the LAN up.
  • Restart HA while charging is in progress.
  • Stop the integration and check that writes get blocked.
  • Restore the integration; old commands shouldn’t fire all at once.
  • Turn off the automation helper; Deye’s local profile should keep working.

16. Entity map to fill in

Fill in the map with your own entities before copying any automation.

SOC
sensor.deye_battery_soc
Your entity: __________
Grid power
sensor.deye_grid_power
Your entity: __________
Minimum SOC
number.deye_minimum_soc
Your entity: __________
Grid Charge
switch.deye_program_1_grid_charge
Your entity: __________
Program SOC
number.deye_program_1_soc
Your entity: __________
Tomorrow’s forecast
sensor.energy_production_tomorrow
Your entity: __________
Load
switch.bojler_nadwyzka
Your entity: __________
Technical dashboard: put every entity on it, along with the freshness sensor, the main switch and the last Logbook entry.

17. The rules that matter most

An effective system is built from small layers: correct data, helpers, a write lock, simple decisions and a local schedule.

  • Don’t control anything off raw PV power — use import or export.
  • Never write on unknown, unavailable, or stale data.
  • Don’t touch all six programs before you’ve confirmed a single entity.
  • Don’t treat a forecast as a certainty.
  • Don’t treat Home Assistant as electrical protection.
  • Keep a manual kill switch for the automation.
  • Read back after every write.
  • Leave the inverter and BMS with the final say.

YAML correctness check

Every block has been re-checked as valid YAML. The automation syntax matches the current Home Assistant format, with triggers, conditions and actions fields. What can’t be automatically confirmed, though, is whether the entity names and settings actually mean what you expect on your specific Deye — that depends on your integration and model.

Mandatory test: once you’ve swapped in your entities, save the automation and open its Traces. Then make one small, reversible change and compare the result against the inverter’s screen. Syntactically correct code can still control the wrong setting if you’ve pointed it at the wrong entity.

Checklist before the first real write

☐ The inverter is visible in Home Assistant.
☐ SOC and grid power match the Deye screen.
☐ You know the sign convention for import and export.
☐ You have a confirmed write entity for your model.
☐ A small, manual, reversible change actually works.
☐ Simulation mode showed sensible decisions.
☐ The main switch stops the automation.

Operating profiles instead of automations fighting each other

The clearest approach is a profile system. Instead of separately setting random fields, automations pick a profile: Summer, Winter, Cheap tariff, Reserve, Away or Manual. One central script translates the profile into the inverter’s actual settings.

input_select:
  deye_profil:
    name: "Deye — operating profile"
    options:
      - Manual
      - Summer
      - Winter
      - Cheap tariff
      - Reserve
      - Away
    initial: Manual

input_boolean:
  deye_automatyka:
    name: "Deye — automation enabled"
  deye_tryb_testowy:
    name: "Deye — simulation only"
Why a profile helps: the dashboard shows your strategy at a glance, it’s easier to fall back to manual, and you can trace the history of decisions.

A central write script with checks and read-back

Entity names here are placeholders. The pattern first sets the SOC target, then Grid Charge, waits for the data to refresh, and checks whether the inverter actually returned the expected value.

script:
  deye_ustaw_okno_ladowania:
    alias: "Deye — set charging window"
    mode: queued
    max: 2
    fields:
      cel_soc:
        required: true
      grid_charge:
        required: true
    sequence:
      - condition: state
        entity_id: input_boolean.deye_automatyka
        state: "on"
      - condition: state
        entity_id: binary_sensor.deye_podstawowe_dane_dostepne
        state: "on"
      - variables:
          wymagany_soc: "{{ cel_soc | int }}"
          wymagany_grid: "{{ grid_charge | bool }}"
      - choose:
          - conditions: "{{ is_state('input_boolean.deye_tryb_testowy', 'on') }}"
            sequence:
              - action: logbook.log
                data:
                  name: "Deye — simulation"
                  message: >
                    Would set SOC to {{ wymagany_soc }}%,
                    Grid Charge {{ wymagany_grid }}.
        default:
          - action: number.set_value
            target:
              entity_id: number.deye_program_1_soc
            data:
              value: "{{ wymagany_soc }}"
          - delay: "00:00:03"
          - choose:
              - conditions: "{{ wymagany_grid }}"
                sequence:
                  - action: switch.turn_on
                    target:
                      entity_id: switch.deye_program_1_grid_charge
            default:
              - action: switch.turn_off
                target:
                  entity_id: switch.deye_program_1_grid_charge
          - delay: "00:00:10"
          - if:
              - condition: template
                value_template: >
                  {{ states('number.deye_program_1_soc') | int(-1)
                     != wymagany_soc }}
            then:
              - action: notify.notify
                data:
                  title: "Deye — write not confirmed"
                  message: >
                    Expected SOC {{ wymagany_soc }}%,
                    read back {{ states('number.deye_program_1_soc') }}%.
              - stop: "Inverter did not confirm the SOC"
Match the services to your own entities: other integrations may use select.select_option, time.set_value, MQTT, or their own services.

Watchdog: reacting to stale data

An automation can’t make decisions off a value from several hours ago. A watchdog shuts off surplus loads and flags the problem.

alias: "Deye — data watchdog"
triggers:
  - trigger: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    to: "off"
    for: "00:03:00"
actions:
  - action: switch.turn_off
    target:
      entity_id:
        - switch.bojler_nadwyzka
        - switch.ladowarka_ev_nadwyzka
  - action: notify.notify
    data:
      title: "Deye — no fresh data"
      message: >
        Load control has been stopped.
        Check the integration, logger or Modbus.
mode: single
The safe state depends on the device: a boiler can usually just be switched off, but a heat pump or a critical system may need a different response.

Extra scenarios worth building

Notify without controlling — the best first step

For a few days, Home Assistant can simply tell you what decision it would have made, based on the forecast, price and SOC. Only once you’ve compared the recommendation against what actually happened in the house do you enable real writes.

alias: "Deye — overnight SOC recommendation"
triggers:
  - trigger: time
    at: "20:30:00"
actions:
  - variables:
      prognoza: "{{ states('sensor.energy_production_tomorrow') | float(-1) }}"
      soc: "{{ states('sensor.deye_battery_soc') | float(-1) }}"
      rekomendacja: >
        {% if prognoza < 0 or soc < 0 %}
          No reliable data — use the manual profile.
        {% elif prognoza >= 18 %}
          Sunny day ahead — leave room for PV.
        {% elif prognoza >= 8 %}
          Moderate production — top up partially.
        {% else %}
          Weak production — consider a higher overnight SOC.
        {% endif %}
  - action: notify.notify
    data:
      title: "Deye — recommendation"
      message: "{{ rekomendacja }}"
mode: single

G12w and weekends

The hours of the cheap zone depend on your contract. Store them in helpers. An automation can pick a weekday or weekend profile, but shouldn’t hard-code hours as if they were universal.

Dynamic prices

Better than a simple threshold is picking a set number of the cheapest hours. You need to factor in the retail price, variable fees, cycle efficiency, expected consumption and available battery capacity.

A negative price doesn’t always mean free energy: check the full billing method and your connection’s power limit.

Away mode

Limits unnecessary cycling and blocks surplus loads. The recommended storage SOC, though, depends on your battery’s manufacturer, so this article doesn’t dictate a single value.

Time-of-day-dependent reserve

In the evening you might keep more energy for the night, in the morning leave room for PV, and before an expected outage raise the reserve with a manual helper.

PV surplus without rapid load cycling

Real surplus isn’t the same as PV production. Stable control needs two thresholds, a confirmation delay, a minimum run time and an import limit.

alias: "Deye — boiler from PV surplus"
triggers:
  - trigger: numeric_state
    entity_id: sensor.deye_export_power
    above: 2200
    for: "00:03:00"
    id: start
  - trigger: numeric_state
    entity_id: sensor.deye_grid_import_power
    above: 500
    for: "00:01:00"
    id: stop_import
  - trigger: numeric_state
    entity_id: sensor.deye_export_power
    below: 500
    for: "00:03:00"
    id: stop_surplus
conditions:
  - condition: state
    entity_id: input_boolean.deye_automatyka
    state: "on"
  - condition: state
    entity_id: binary_sensor.deye_podstawowe_dane_dostepne
    state: "on"
actions:
  - choose:
      - conditions: "{{ trigger.id == 'start' }}"
        sequence:
          - condition: numeric_state
            entity_id: sensor.deye_battery_soc
            above: 80
          - action: switch.turn_on
            target:
              entity_id: switch.bojler_nadwyzka
      - conditions: "{{ trigger.id in ['stop_import', 'stop_surplus'] }}"
        sequence:
          - action: switch.turn_off
            target:
              entity_id: switch.bojler_nadwyzka
mode: restart
Electrical safety: Home Assistant doesn’t replace a thermostat, over-temperature protection, a contactor, or a correct installation.

Heat pump

Prefer SG Ready, the manufacturer’s Modbus interface, changing the setpoint, or an official self-consumption mode. Don’t cut the compressor like an ordinary outlet, and keep minimum run times.

Electric car

Control the charger’s current, factoring in minimum current, phase count, your connection limit and a delay between changes. The house’s reserve should have higher priority than the EV.

Does a battery cycle actually pay off?

An automation can be technically correct and still increase cost. Compare the charging price against the price of the purchase it avoided, the round-trip efficiency, and the cost you assign to battery wear.

cost of stored energy ≈ charging price ÷ round-trip efficiency + battery wear cost

We’re not giving a single efficiency figure or wear cost. They depend on the battery, the inverter, temperature, power, and how you choose to value degradation.

Good practice: set a minimum required price gap and skip the cycle when the projected saving is only symbolic.

Operator dashboard

One card should show the system’s state and the reason behind the automation’s latest decision.

  • Automation and test mode.
  • Current profile and the reason for the last change.
  • SOC, battery, import, export, PV and load.
  • Data freshness and the last valid frame.
  • Target SOC, Grid Charge and the active TOU period.
  • Forecast, price and surplus loads.

Sources and current references

Home Assistant’s documentation confirms that production forecasts are used in automations. The Deye/Sunsynk project’s definitions show which entities are available for read/write. SolarAssistant recommends testing changes first with writes disabled, before enabling actual control. The more elaborate adaptive-charging and EMHASS examples are community patterns and need adapting to your setup.

Sources and materials

Społeczność MarkLabs

Dyskutuj na forum

Pytania, doświadczenia i rozwiązania do tego artykułu publikujemy teraz na forum MarkLabs.

Otwórz dyskusję na forum

Wymagane jest bezpłatne konto MarkLabs. Dotychczasowe komentarze bloga nie są przenoszone.

buymeacoffee.com