A HACS integration for Home Assistant that optimizes self-consumption of solar energy with battery, EV charger, and electric boiler/water heater.
Läs detta på svenska: README.sv.md
- Fixed a periodic single-cycle power glitch during otherwise-stable charging/discharging. Sonnen's API description ("the setpoint is kept until the battery receives a new charging or discharging value") reads as ONE shared internal setpoint (direction + magnitude) behind the separate "force charge"/"force discharge" number entities, not two independent registers — the latest write wins regardless of which direction issued it. P6-1's heartbeat (re-send an unchanged value at least every 5 minutes, for self-healing against a dropped write) was rewriting the inactive direction's 0 on that schedule even while the other direction was actively driving a real value — on a shared setpoint, that briefly zeroes it until the next cycle's write catches up, visible as a single ~30s near-zero blip in
battery_inoutroughly every 5-10 minutes during an otherwise clean, continuously-rising charge or discharge ramp._write_battery_setpoints()now only heartbeat-rewrites the inactive direction's 0 when both directions are meant to be idle (the case the self-healing actually protects); while one direction is active, the other is only touched for a genuine correction, never on the heartbeat schedule alone.
See CHANGELOG.md for older releases (including v0.7.5's manual-mode fix and v0.7.6's evening-target fix).
Grid (3-phase, max 20A/phase)
│
│ Battery inverter (3-phase) Solar inverter (3-phase)
│ │ │
└─────┬─────┴─────────────────────────────┴──── Meter 4 (house load) ────┬──── Other loads
│ │
│ Meter 5
│ │
│ Boiler
│ (1-phase compressor
│ +2-phase heating element
└────┐ +buffer tank)
│
EV charger
(1- or 3-phase hardware,
multiple cars take turns,
car selection via HA entity)
NOTE: The charger's number of phases (hardware) and the car's number of phases (
car_phases, 1/2/3-phase) are two separate settings. Phase load in the phase protection is always governed by the car's built-in charger, not by the charger hardware's phase count – see EV Charger and Car Selection.
| Meter | Location | Sign | Unit |
|---|---|---|---|
| Meter 1 | Grid connection | Negative = export, Positive = import | kW |
| Meter 2 | Battery inverter AC side | Negative = consumption | W |
| Meter 3 | Solar inverter | Positive = production | W |
| Meter 4 | Property load (excl. solar, battery, and EV charger) | Positive = consumption | W |
| Meter 5 | Boiler | Positive = consumption | W |
NOTE: Meter 1 reports in kW – select the unit
kWin the configuration. The EV charger (internal meter) also reports in kW.
- Cover household load – priority 1
- Charge cars from solar – when solar surplus ≥ 1,400 W (1-phase) or 4,140 W (3-phase); current adjusted dynamically
- Charge battery from solar surplus
- Extra hot water via heating element – when the battery is full, solar surplus remains, and tank temperature is below the configured maximum (default 70°C)
- Discharge the battery – when the buy price exceeds SEK 0.20/kWh, or when it is the best discharge hour in the coming 12h according to the price planning
- Negative electricity prices – absorb all possible solar power into battery, cars, and hot water
Based on Nord Pool's raw_today/raw_tomorrow attributes, the following are calculated each cycle:
- Best charging/discharging hour for the coming 12h – governs battery decisions in both auto and winter mode
- Proactive absorption – if ≥ 4 quarter-hours with negative sell price are expected within 2h, the battery holds headroom (up to 30%) to make room for solar. Extra hot water and EV charging are not started proactively; they wait until the price is actually negative
- Results are exposed via
sensor.sem_negative_slots_ahead,sensor.sem_best_discharge_price, andsensor.sem_best_charge_price
If Solcast sensors are configured, the detailedForecast attribute (30-minute pv_estimate values in kW) is read each cycle and matched against Nord Pool price slots:
- Per-slot solar data – each price slot gets an expected solar power value (kW) and energy (kWh)
- Wait for solar – if ≥ 2 kWh of solar is expected within 2 h and the current buy price is above SEK 0.50/kWh, battery charging from the grid is skipped to preserve headroom for free solar
- Solar aggregates – rolling forecasts for the next 2 h, 4 h, and 8 h exposed as sensors
- Peak solar – the expected peak power (kW) within 8 h and how many hours until it occurs
sensor.sem_wait_for_solarturnsonwhen the system is holding back grid charging in anticipation of solar
- Charge battery overnight when the price is below a configurable limit
- Discharge battery during expensive hours (evening peak)
- Always charge from solar when possible
- Force EV Charge – charge all connected cars from the grid (max current, phase-limited)
- Force Battery Charge – charge battery from the grid
- Manual – disables all automatic control. No decisions are made; used when you want to control battery/charger/hot water manually via your own automations. Selected via
select.sem_operating_mode(there is no dedicated switch for this mode).
Calculates phase load per phase and reduces in priority order:
- Reduces EV charging current (lowest-priority charger first)
- Reduces battery charging
- Turns off extra hot water (heating element)
Each EV charger is configured as a hardware unit with a list of cars that can use it. When a car is connected, the user selects which car it is via a select entity in the dashboard.
Charger A (3-phase hardware)
├── Connection sensor: sensor.charger_status
├── Car 1: Volvo XC40 (SOC sensor, target 80%, 1-phase car charger, phase L1)
└── Car 2: Tesla Model 3 (SOC sensor, target 90%, 3-phase car charger)
The phase load that the phase protection calculates is always determined by the car's car_phases (1/2/3-phase), not by the charger hardware's phase count. A 1-phase car only loads its selected phase; a 2-phase car loads its phase plus the next one (e.g., L1+L2); a 3-phase car loads all three phases equally.
- Car is connected →
sensor.sem_charger_a_connected→on - The system pauses charging and sends a persistent HA notification
- The user selects the car in
select.sem_charger_a_active_car - The system charges using the correct SOC target and phase setting for the selected car
- The notification closes automatically
| Setting | Description |
|---|---|
| Name | Display name for the charger |
| Connection sensor | Sensor showing connected/charging/disconnected |
| Charger switch | Switch to enable/disable charging |
| Charging current setpoint | Number entity for current setting (A) |
| Charging power sensor | Sensor for actual charging power (optional) |
| Number of phases | 1-phase or 3-phase (hardware) |
| Setting | Description |
|---|---|
| Name | Display name for the car |
| SOC sensor | The car's battery sensor (optional) |
| Target SOC | Charging target in % (default 80%) |
| Number of phases (car's charger) | 1-phase, 2-phase, or 3-phase – the car's built-in charger, governs phase load |
| Phase | Starting phase the car charges on (relevant for 1- and 2-phase cars) |
The boiler has two separate circuits:
| Circuit | Operation | Phases | Typical power |
|---|---|---|---|
| Heat pump (compressor) | Normal house heating | 1-phase (configurable, default L3) | 500–1,500 W |
| Heating element | Extra hot water | 2-phase (the two remaining phases, default L1+L2) | 3,000–6,000 W |
The element phases are calculated automatically as the two phases not used by the compressor.
A temperature sensor on the buffer tank is used to prevent unnecessary heating:
- Max temp (default 70°C): turns off extra hot water if the tank reaches this level
- Min temp (default 65°C): does not start extra hot water until the tank is below this level (even if everything else allows it)
- The temperature is shown in
sensor.sem_hot_water_temp
To prevent the heating element from switching on/off every 30-second cycle, extra hot water is forced to stay ON for at least 5 minutes (default, configurable) from the actual start time, even if the control logic wants to turn it off earlier.
The boiler's built-in Legionella program runs automatically about once a week to heat the hot water to ≥ 65°C (configurable) and eliminate Legionella bacteria.
The system uses a separate digital switch to start the boiler's Legionella program:
- We turn the switch ON to start the program
- The boiler finishes the program and turns the switch OFF automatically when done
- If the switch is turned off prematurely, the cycle is aborted and the run is not counted as successful
- The run is confirmed via the temperature sensor – if the temperature does not reach the target value, the run is not registered as successful
| Priority | Condition | Description |
|---|---|---|
| 1 | Solar surplus ≥ 3,000 W within the desired time window | Free solar power drives the program |
| 2 | Electricity price ≤ configured max price within the desired time window | Runs on cheap grid power |
| 3 | Interval exceeded by 50% (emergency run) | Runs regardless of price, avoids the night 23:00–06:00 |
| Setting | Default | Description |
|---|---|---|
| Enabled | Yes | Turn the feature on/off |
| Legionella switch | – | The boiler's digital program switch |
| Confirmation temp | 65°C | Temperature that confirms a successful run |
| Interval | 7 days | How often disinfection should occur |
| Desired time window | 10–15 | Hours when solar is normally available |
| Max price | SEK 1.50/kWh | Do not run on grid power if more expensive |
| Run time | 60 min | Reference time (the boiler controls the actual time) |
| Formula | |
|---|---|
| Buy price | (spot price + grid fees + energy tax) × (1 + VAT) |
| Sell price | spot price + extra revenue (electricity certificates, etc.) |
| Entity | Description |
|---|---|
sensor.sem_buy_price |
Current buy price SEK/kWh |
sensor.sem_sell_price |
Current sell price SEK/kWh |
sensor.sem_spot_price |
Nord Pool spot price |
sensor.sem_battery_charge_power |
Battery charging setpoint (W) |
sensor.sem_battery_discharge_power |
Battery discharging setpoint (W) |
sensor.sem_phase_l1_load |
Calculated phase load L1 (W) |
sensor.sem_phase_l2_load |
Calculated phase load L2 (W) |
sensor.sem_phase_l3_load |
Calculated phase load L3 (W) |
sensor.sem_house_load |
House load W – Meter 4 direct or calculated |
sensor.sem_solar_surplus |
Solar surplus (W) |
sensor.sem_hot_water_temp |
Buffer tank temperature (°C) |
sensor.sem_decision_reason |
Text explanation of the latest decision |
sensor.sem_operating_mode |
Active operating mode |
sensor.sem_legionella_active |
on when the Legionella program is running |
sensor.sem_legionella_days_since |
Days since the last confirmed run |
sensor.sem_legionella_next_due |
Date of the next planned run |
sensor.sem_legionella_temp_confirmed |
on if temp is confirmed during an ongoing run |
sensor.sem_negative_slots_ahead |
Number of quarter-hours with negative sell price in the coming 8h |
sensor.sem_best_discharge_price |
Best (highest) buy price for discharging in the coming 12h, with timestamp as attribute |
sensor.sem_best_charge_price |
Lowest buy price for charging in the coming 12h, with timestamp as attribute |
sensor.sem_yesterday_consumption |
Yesterday's consumption excl. EV charging (kWh) – requires a configured sensor |
sensor.sem_solar_next_2h_kwh |
Expected solar energy next 2 h (kWh, Solcast median) |
sensor.sem_solar_next_4h_kwh |
Expected solar energy next 4 h (kWh, Solcast median) |
sensor.sem_solar_next_8h_kwh |
Expected solar energy next 8 h (kWh, Solcast median) |
sensor.sem_peak_solar_kw_next_8h |
Expected peak solar power within 8 h (kW) – attribute: peak_solar_time |
sensor.sem_hours_to_solar_peak |
Hours until solar peak within 8 h |
sensor.sem_wait_for_solar |
on when the system is holding back grid charging in anticipation of solar |
sensor.sem_battery_accumulated_cost |
Accumulated cost (SEK) of energy currently in the battery – attributes: solar_kwh_total, grid_kwh_total, average_price_sek_kwh, last_sell_price |
sensor.sem_battery_average_price |
Average cost per kWh of energy currently in the battery (SEK/kWh) |
Per charger (replace <charger> with the charger's name in lowercase):
| Entity | Description |
|---|---|
sensor.sem_charger_<charger>_connected |
on/off – car physically connected |
sensor.sem_charger_<charger>_active_car |
Name of the selected car |
sensor.sem_charger_<charger>_current |
Charging current setpoint (A) |
sensor.sem_charger_<charger>_enabled |
Charging active on/off |
sensor.sem_phase_lX_loadis a forecast, not a measurement – it reflects the calculated phase load after the control decisions have been executed.
| Entity | Function |
|---|---|
switch.sem_force_ev_charge_from_grid |
Force EV charging from the grid |
switch.sem_winter_mode |
Enable winter mode |
switch.sem_force_charge_battery_from_grid |
Force battery charging |
| Entity | Function |
|---|---|
select.sem_operating_mode |
Select operating mode: auto / winter / force_charge_ev / force_charge_battery / manual |
select.sem_charger_<charger>_active_car |
Select which car is connected to the charger |
| Entity | Function |
|---|---|
number.sem_battery_min_soc |
Battery min SOC % |
number.sem_battery_max_soc |
Battery max SOC % |
number.sem_ev_soc_target |
Global default charging target % |
number.sem_winter_cheap_threshold |
Cheap price threshold (SEK/kWh) |
number.sem_winter_expensive_threshold |
Expensive price threshold (SEK/kWh) |
number.sem_winter_min_soc |
Winter min SOC % |
number.sem_winter_max_soc |
Winter max SOC % |
- Go to HACS → Integrations → ⋮ → Custom repositories
- Add
https://github.com/fjonson95/smart_energy_manager, category: Integration - Install "Smart Energy Manager"
- Restart Home Assistant
- Settings → Integrations → Add → Smart Energy Manager
One of these Nord Pool integrations must be installed:
- nordpool (HACS) –
custom_components/nordpool, providesraw_today/raw_tomorrowwith 15-minute prices - Nord Pool (official) – built-in HA integration (HA 2024.x+), requires
nordpool_type = officialandnordpool_area(e.g.SE3) in configuration
Optional but recommended:
- solcast_solar – solar forecast
Configuration takes place in six steps:
Step 1 – Grid & Pricing
- Nord Pool sensor (required) – spot price sensor from the chosen integration
- Nord Pool integration type –
hacs(default) orofficial - Nord Pool price area – e.g.
SE3(only used withofficial) - Grid meter per phase (L1/L2/L3)
- Current sensors per phase (for phase protection)
- Max current per phase (default 20 A)
- Grid voltage (default 230 V)
- Grid fees, energy tax, VAT, sales compensation
- House load controller – point to Meter 4 for direct house load measurement (recommended)
- Grid meter unit – select
kWif Meter 1 reports in kilowatts - EV charger power unit – select
kWif the charger reports in kilowatts - Yesterday's consumption – optional sensor for
sensor.sem_yesterday_consumption
Step 2 – Solar Panels
- Solar inverter total and per phase
- Solcast forecasts today/tomorrow
Step 3 – Battery
- SOC sensor, power sensor, charging and discharging entities
- Capacity (kWh) and max power (kW)
- Min/max SOC limits
Step 4 – Electric Boiler / Heat Pump
- Power sensor (Meter 5) – the compressor's on/off status is read from the power, no separate switch needed
- Switch for extra hot water (heating element)
- Compressor phase (1-phase, default L3) – element phases are calculated automatically
- Heating element rated power (kW)
- Buffer tank temperature sensor (optional)
- Max temp for extra hot water (default 70°C)
- Min temp for extra hot water (default 65°C)
- Minimum runtime for extra hot water (default 5 min) – prevents on/off flicker
Step 5 – Legionella Disinfection
- Enable/disable the feature
- Legionella switch (the boiler's digital program switch)
- Confirmation temp (default 65°C – the run is approved when the tank reaches this temp)
- Interval in days (default 7)
- Desired time window (default 10–15)
- Max electricity price for running on grid power (default SEK 1.50/kWh)
- Run time in minutes (reference time)
Step 6 – EV Charger
- Add one or more chargers
- Per charger: name, connection sensor, switch, current setpoint entity, charger phase count (1-phase or 3-phase hardware)
- Per car on the charger: name, SOC sensor, SOC target, the car's phase count (1/2/3-phase built-in charger – governs phase load), phase (for 1- and 2-phase cars)
- Repeat for each charger
All settings can be edited afterwards via Settings → Integrations → Smart Energy Manager → Configure.
| Sensor | Unit in HA | Setting |
|---|---|---|
| Meter 1 (grid meter) | kW | Grid meter unit → kW |
| Meter 3 / SolInv_prod | W | (default W) |
| BatInv_in_out | W, pos=charging, neg=discharging | (default W) |
| Meter 4 (house load) | W | House load controller → Meter 4 |
| Meter 5 (boiler) | W | Boiler power sensor → Meter 5 |
| Car_charging (internal) | kW | EV charger power unit → kW |
logger:
logs:
custom_components.smart_energy_manager: debugautomation:
- alias: "Winter mode October–March"
trigger:
- platform: time
at: "00:01:00"
condition:
- condition: template
value_template: "{{ now().month in [10,11,12,1,2,3] }}"
action:
- service: select.select_option
target:
entity_id: select.sem_operating_mode
data:
option: wintertype: vertical-stack
cards:
- type: entity
entity: select.sem_operating_mode
name: Operating mode
- type: glance
entities:
- entity: sensor.sem_buy_price
name: Buy SEK/kWh
- entity: sensor.sem_sell_price
name: Sell SEK/kWh
- entity: sensor.sem_solar_surplus
name: Solar surplus W
- entity: sensor.sem_house_load
name: House load W
- entity: sensor.sem_hot_water_temp
name: Hot water °C
- type: gauge
entity: sensor.sem_battery_charge_power
name: Battery charging W
max: 5000
- type: entities
title: Charger A
entities:
- entity: sensor.sem_charger_laddare_a_connected
name: Connected
- entity: select.sem_charger_laddare_a_active_car
name: Selected car
- entity: sensor.sem_charger_laddare_a_current
name: Charging current A
- entity: sensor.sem_charger_laddare_a_enabled
name: Charging active
- type: entities
title: Legionella
entities:
- entity: sensor.sem_legionella_active
name: In progress
- entity: sensor.sem_legionella_temp_confirmed
name: Temp confirmed
- entity: sensor.sem_legionella_days_since
name: Days since last
- entity: sensor.sem_legionella_next_due
name: Next run
- type: entity
entity: sensor.sem_decision_reason
name: Latest decision