-
-
Notifications
You must be signed in to change notification settings - Fork 6
ev surplus charging automation
This guide explains how to wire your physical EV charger to follow HSEM's
surplus-charging recommendations. HSEM calculates how much power the EV should
draw (ev_charger_calculated_power), but the charger itself needs a different
signal — it needs to know the grid surplus so it can dynamically adjust.
HSEM's ev_charger_calculated_power (from sensor.hsem_workingmode_sensor
→ hourly_recommendation) tells you the target AC power the EV should
draw right now. For example, 2900 means "charge at 2.9 kW."
Dynamic chargers (go-e, Easee, Zaptec) don't accept a direct charge-power command. Instead, they monitor the grid import/export and adjust their draw to keep the grid near zero — consuming exactly the available surplus.
If you send pGrid = -2900 (exporting 2.9 kW), the charger sees surplus and
ramps up. But once it reaches 2.9 kW, the grid is balanced and pGrid should
be near zero. If you keep sending -2900, the charger thinks there's still
surplus and may overshoot or behave erratically.
The fix: subtract the charger's current actual power from the target.
pGrid = (ev_charger_calculated_power × -1) + current_charge_power
| Variable | Source | Example |
|---|---|---|
ev_charger_calculated_power |
state_attr('sensor.hsem_workingmode_sensor', 'hourly_recommendation').ev_charger_calculated_power |
2900 W |
current_charge_power |
Your charger's power sensor (W) |
2000 W |
pGrid |
Sent to charger |
-900 W |
Worked example:
- HSEM says charge at 2900 W →
target = 2900 - Charger is currently drawing 2000 W →
charge = 2000 pGrid = -2900 + 2000 = -900
The charger sees 900 W of remaining surplus and increases its draw. When it
reaches 2900 W, pGrid = -2900 + 2900 = 0, and the charger holds steady —
grid is balanced.
When HSEM says 0 W (no surplus), pGrid = 0 + 2000 = +2000, signaling
the charger that it's importing from the grid and should back off.
Always guard against unavailable sensors. If sensor.hsem_workingmode_sensor
is offline, state_attr() returns None and accessing .ev_charger_calculated_power
will crash the template.
Use this pattern in all charger automations:
{% set rec = state_attr('sensor.hsem_workingmode_sensor', 'hourly_recommendation') %}
{% set target = rec.ev_charger_calculated_power | int(0) if rec is not none else 0 %}
{% set charge = states('sensor.<your_charger_power>') | int(0) %}
{{ (target * -1 + charge) }}If the HSEM sensor is unavailable, target defaults to 0 and the charger
is told to back off rather than the template crashing.
go-e chargers accept pGrid, pAkku, and pPv via MQTT on the
go-eCharger/<serial>/ids/set topic.
The recommended approach combines two signals:
-
pGrid— fed with your real grid power sensor every few seconds. The go-e charger's built-in PID loop chases actual surplus in real time, responding to clouds and load changes instantly. -
amp(requested current) — set to HSEM'sev_charger_calculated_powerconverted to amps. This acts as a ceiling — the charger will never draw more than the MILP-optimized target, but it will draw less (or nothing) when real surplus is low.
Why two signals? HSEM recalculates
ev_charger_calculated_powerevery 5 minutes (the planner's update interval). If you feed that directly intopGrid, the charger can't respond to a passing cloud — it keeps drawing the old target and imports from the grid. With real grid power inpGrid, the charger adjusts second-by-second. Theampceiling ensures it never exceeds what HSEM planned.
Why
pGridinstead of setting amps only? Theidstopic (which carriespGrid) is designed for frequent updates — the value decays after 10–15 seconds and is expected to be refreshed continuously. Setting the charge current via config keys likeampwrites to persistent storage, so we only update it when HSEM recalculates (every 5 minutes). See the go-eCharger MQTT docs for details.
| Entity | Purpose |
|---|---|
sensor.hsem_workingmode_sensor |
HSEM hourly recommendation with ev_charger_calculated_power
|
sensor.power_meter_active_power |
Real grid import/export power in watts (negative = export) |
binary_sensor.go_echarger_<serial>_car |
on when car is plugged in |
number.go_echarger_<serial>_amp |
Requested current number entity (for the ceiling) |
This feeds actual grid power into pGrid so the charger chases real surplus.
alias: go-e Surplus Signal from Grid Power
description: >-
Feeds real grid power into go-e charger's pGrid so it dynamically
follows actual solar surplus second-by-second.
triggers:
- trigger: time_pattern
seconds: /3
conditions:
- condition: state
entity_id: binary_sensor.go_echarger_222819_car
state: "on"
actions:
- data:
qos: "0"
topic: go-eCharger/222819/ids/set
retain: false
payload: |-
{% set grid = states('sensor.power_meter_active_power') | int(0) %}
{
"pGrid": "{{ grid }}",
"pAkku": "0",
"pPv": "0"
}
action: mqtt.publish
mode: singleThis sets the maximum charge current from HSEM's MILP-optimized target. Only updates when the target changes, avoiding unnecessary writes to persistent storage.
alias: go-e Charge Ceiling from HSEM
description: >-
Sets go-e charger's maximum current from HSEM's ev_charger_calculated_power.
Acts as a ceiling — the charger won't exceed this, but will draw less
when real surplus is low.
triggers:
- trigger: time_pattern
minutes: /5
- trigger: state
entity_id:
- sensor.hsem_workingmode_sensor
conditions:
- condition: state
entity_id: binary_sensor.go_echarger_222819_car
state: "on"
actions:
- variables:
rec: >-
{{ state_attr('sensor.hsem_workingmode_sensor', 'hourly_recommendation') }}
target_w: >-
{{ rec.ev_charger_calculated_power | int(0) if rec is not none else 0 }}
target_amps: >-
{{ (target_w / 690) | round(0, 'floor') | int }}
clamped_amps: >-
{{ ([6, target_amps, 32] | sort)[1] }}
- if:
- condition: template
value_template: >-
{{ (clamped_amps - states('number.go_echarger_222819_amp') | int(0)) | abs > 0 }}
then:
- action: number.set_value
target:
entity_id: number.go_echarger_222819_amp
data:
value: "{{ clamped_amps }}"
mode: singleNotes:
- Replace
222819with your charger's serial number.- Replace
sensor.power_meter_active_powerwith your actual grid power sensor (negative = export, positive = import).- Replace
690with230if you have a single-phase installation.clamped_ampskeeps the ceiling between 6 A (minimum most EVs accept) and 32 A (typical installation limit). Adjust to match your circuit breaker.- The
pAkkuandpPvfields are set to0because HSEM manages the home battery separately — the charger only needs the grid surplus signal.- The ceiling automation only calls
number.set_valuewhen the target actually changes, avoiding writes to persistent storage on every tick.
Easee chargers are controlled via the Easee EV Charger
integration. The integration exposes a easee.set_charger_dynamic_limit service
that sets the maximum charging current in amps.
Unlike go-e, Easee does not use a grid-surplus signal. Instead, you convert HSEM's power target directly to amps and set it as the charger's dynamic limit.
- Convert HSEM's
ev_charger_calculated_power(watts) to amps - Call
easee.set_charger_dynamic_limitwith the amp value - Use
time_to_liveso the limit expires if HA stops updating
Amps formula:
amps = ev_charger_calculated_power / (voltage × phases)
For a typical European 3-phase setup: amps = power_w / (230 × 3) ≈ power_w / 690.
For single-phase: amps = power_w / 230.
| Entity | Purpose |
|---|---|
sensor.hsem_workingmode_sensor |
HSEM hourly recommendation with ev_charger_calculated_power
|
binary_sensor.easee_<id>_cable_connected |
on when car is plugged in |
sensor.easee_<id>_power |
Current charging power in watts (for diagnostics) |
Note: You also need an Easee Equalizer (HAN/Nevion) installed for dynamic current limiting to work. Without it, the charger ignores dynamic current commands.
alias: Easee Surplus Charging from HSEM
description: >-
Sets Easee charger dynamic current limit based on HSEM's
EV charging recommendation.
triggers:
- trigger: time_pattern
seconds: /10
conditions:
- condition: state
entity_id: binary_sensor.easee_12345_cable_connected
state: "on"
actions:
- variables:
rec: >-
{{ state_attr('sensor.hsem_workingmode_sensor', 'hourly_recommendation') }}
target_w: >-
{{ rec.ev_charger_calculated_power | int(0) if rec is not none else 0 }}
target_amps: >-
{{ (target_w / 690) | round(0, 'floor') | int }}
clamped_amps: >-
{{ ([0, target_amps, 32] | sort)[1] }}
- if:
- condition: template
value_template: "{{ target_amps > 0 }}"
then:
- action: easee.set_charger_dynamic_limit
data:
charger_id: "12345"
current: "{{ clamped_amps }}"
time_to_live: 30
else:
- action: easee.set_charger_dynamic_limit
data:
charger_id: "12345"
current: 0
time_to_live: 30
mode: singleNotes:
- Replace
12345with your charger ID (find it in the Easee integration device list).- Replace
690with230if you have a single-phase installation.clamped_ampskeeps the value between 0 and 32 A (Easee's range is 0–40 A; adjust the upper bound to match your installation's circuit breaker).time_to_live: 30means the limit expires after 30 seconds if HA stops sending updates — a safety net that prevents the charger from getting stuck at a high limit.- The trigger interval is
/10seconds (not/3like go-e) because Easee's cloud API has rate limits. Do not go below 5 seconds.
If you have multiple chargers on one circuit, use easee.set_circuit_dynamic_limit
instead:
actions:
- action: easee.set_circuit_dynamic_limit
data:
circuit_id: 12345
current_p1: "{{ clamped_amps }}"
current_p2: "{{ clamped_amps }}"
current_p3: "{{ clamped_amps }}"
time_to_live: 30Zaptec chargers are controlled via the Zaptec
integration. The integration exposes a number.<name>_available_current entity
on the Installation device that sets the maximum charging current in amps.
Like Easee, Zaptec does not use a grid-surplus signal. You convert HSEM's power target to amps and set it as the available current.
Important: Zaptec recommends not changing the available current more often than every 15 minutes. The automation below uses a 15-second trigger for responsiveness but only calls the service when the target changes significantly.
| Entity | Purpose |
|---|---|
sensor.hsem_workingmode_sensor |
HSEM hourly recommendation with ev_charger_calculated_power
|
binary_sensor.zaptec_<name>_connected |
on when car is plugged in |
number.<installation_name>_available_current |
Sets max current for the installation |
Before you start: Disable Zaptec Sense (APM/Automatic Power Management) and stand-alone mode in the Zaptec Portal. The charger must be in cloud-managed mode for the current limit to take effect.
alias: Zaptec Surplus Charging from HSEM
description: >-
Sets Zaptec installation available current based on HSEM's
EV charging recommendation.
triggers:
- trigger: time_pattern
seconds: /15
conditions:
- condition: state
entity_id: binary_sensor.zaptec_my_charger_connected
state: "on"
actions:
- variables:
rec: >-
{{ state_attr('sensor.hsem_workingmode_sensor', 'hourly_recommendation') }}
target_w: >-
{{ rec.ev_charger_calculated_power | int(0) if rec is not none else 0 }}
target_amps: >-
{{ (target_w / 690) | round(0, 'floor') | int }}
clamped_amps: >-
{{ ([6, target_amps, 32] | sort)[1] }}
- if:
- condition: template
value_template: >-
{{ (target_amps - states('number.my_installation_available_current') | int(0)) | abs > 1 }}
then:
- action: number.set_value
target:
entity_id: number.my_installation_available_current
data:
value: "{{ clamped_amps }}"
mode: singleNotes:
- Replace
my_chargerandmy_installationwith your actual Zaptec entity names.- Replace
690with230if you have a single-phase installation.clamped_ampskeeps the value between 6 A (minimum most EVs accept) and 32 A (typical installation limit). Adjust to match your circuit breaker.- The condition
abs > 1prevents unnecessary API calls — the service is only called when the target changes by more than 1 A.- If you have multiple chargers on one installation, changing the installation-level current affects all of them.
If you need per-charger control (e.g. different cars on different schedules),
use the zaptec.limit_current service instead:
actions:
- action: zaptec.limit_current
data:
device_id: abc123def456
available_current_phase1: "{{ clamped_amps }}"
available_current_phase2: "{{ clamped_amps }}"
available_current_phase3: "{{ clamped_amps }}"This sets the current on a specific charger rather than the whole installation. Find the
device_idin the Zaptec charger device page.
- Home — User-facing overview: features, FAQ, working modes, battery schedules, excess export, consumption sensors
- Battery Charging Economics — How to calculate the minimum charging price for a battery schedule
- Architecture Overview — System context, layered architecture, module map, planning pipeline
- Planner Specification — Normative — all planner invariants, rules, and constraints
- Planner Technical Guide — How the planner works with worked examples
- Cost Function Math — Complete mathematical formulation of the 8-term cost function
- Energy Accounting — Physical energy flow model, SoC simulation, efficiency math
- Candidate Generation — How candidates are generated, assumptions, partial-SoC
- MILP Optimization — Full LP formulation, variable layout, constraints, and solver pipeline
- Consumption Prediction — Weighted-average model, IQR outlier detection, spike suppression
- Safety Modes — Degraded mode, read-only gate, write-verify applier, runtime resolver
- Price Scaling — EDS price scaling, eds_share conversion factor
- Services Reference — All 4 HSEM services with examples
- Sensors Reference — Complete entity reference: all sensor, select, switch, number, and time entities
- Dashboard Setup — Step-by-step ApexCharts dashboard with full YAML, layout reference, and troubleshooting
- Config Flow Reference — Every config/options flow step and field
- EV Charge Plan Setup — EV planned load configuration guide
- EV Surplus Charging Automation — Wire your physical EV charger (go-e, Easee, Zaptec) to follow HSEM surplus recommendations
- EV Optimal Charging Template — Legacy Home Assistant template sensor for cost-optimal EV charging
- Forecast Accuracy Tracking — Forecast vs actual tracking system
- Huawei Entities — Canonical HA entity ID reference
- Troubleshooting Guide — Diagnose and fix common problems: missing data, wrong prices, write failures, battery behaviour
- Quality Checks — Static quality tools and CI configuration