Energy Management with Home Assistant
Overview
EMHASS (Energy Management for Home Assistant) runs on a remote Docker container at 192.168.1.120:5000. It performs day-ahead optimization to schedule 3 deferrable loads at the cheapest energy prices, using dynamic pricing from the Energyzero integration.
EMHASS web UI: http://192.168.1.120:5000/
From here its possible to configure the settings, start optimization runs and see the optimization results in tables and graphs.

Deferrable Loads
| Load | Power | Operating Hours | EMHASS Sensor | Entity ID |
|---|---|---|---|---|
| EV Charger (Peblar) | 11 kW | 7h/day | sensor.p_deferrable0 |
switch.peblar_ev_charger_opladen |
| EV Charger (socket) | 3 kW | 6h/day | sensor.p_deferrable1 |
switch.ledvance_plug_compact_eu_em_t |
| Washing Machine | 2.4 kW | 2h/day | sensor.p_deferrable2 |
input_boolean.fingerbot_button |
Architecture
Energyzero API
↓
Home Assistant (energyzero.get_energy_prices)
↓ 24h price forecast (today + tomorrow)
EMHASS Docker (192.168.1.120:5000/action/dayahead-optim)
↓ computes optimized schedules
EMHASS Docker (192.168.1.120:5000/action/publish-data)
↓ publishes current time step values (every 5 minutes)
sensor.p_deferrable0, sensor.p_deferrable1, sensor.p_deferrable2
↓ immediately after publish
Automations turn loads on/off
EMHASS Docker Setup
Server
EMHASS runs on 192.168.1.120 (same server as RAID storage, Nextcloud, etc.) via Docker Compose.
Docker Compose (/home/bram/compose.yml)
emhass:
image: ghcr.io/davidusb-geek/emhass:latest
container_name: emhass
restart: unless-stopped
ports:
- 5000:5000
volumes:
- /home/bram/emhass/share:/share
- /home/bram/emhass/secrets_emhass.yaml:/app/secrets_emhass.yaml
environment:
- LOGGING_LEVEL=INFO
Secrets (/home/bram/emhass/secrets_emhass.yaml)
Contains the Home Assistant connection details:
hass_url: http://192.168.1.19:8123/
long_lived_token: <long-lived-access-token>
Configuration (/home/bram/emhass/share/config.json)
Key settings:
| Setting | Value | Description |
|---|---|---|
costfun |
profit |
Optimize for maximum profit |
optimization_time_step |
60 |
1-hour time steps |
method_ts_round |
first |
Forecast horizon starts at current timestep (floor), not nearest/next |
historic_days_to_retrieve |
10 |
Days of history for load forecasting |
number_of_deferrable_loads |
3 |
EV 11kW, EV 3kW, Washing machine |
nominal_power_of_deferrable_loads |
[11040, 3000, 2400] |
Watts per load |
operating_hours_of_each_deferrable_load |
[7, 6, 2] |
Hours per day |
weather_forecast_method |
open-meteo |
Free weather forecast source |
load_forecast_method |
naive |
Simple load prediction |
set_use_pv |
true |
PV production considered |
set_use_battery |
false |
No battery storage |
use_websocket |
true |
WebSocket connection to HA |
maximum_power_from_grid |
17250 |
Max grid import (W) |
maximum_power_to_grid |
10000 |
Max grid export (W) |
production_price_forecast_method |
constant |
Fixed PV sell price |
photovoltaic_production_sell_price |
0.4 |
€/kWh (netting rule) |
PV System (2 arrays):
| Parameter | Array 1 | Array 2 |
|---|---|---|
| Module model | 300W | 400W |
| Inverter model | 5000W | 5000W |
| Surface tilt | 35° | 35° |
| Surface azimuth | 180° (South) | 135° (SE) |
| Modules per string | 15 | 15 |
| Strings per inverter | 1 | 1 |
HA Sensor mappings:
| EMHASS Config Key | HA Sensor |
|---|---|
sensor_power_photovoltaics |
sensor.solar_panels_production_power_watts |
sensor_power_load_no_var_loads |
sensor.power_load_no_var_loads |
sensor_replace_zero |
sensor.peblar_ev_charger_vermogen, sensor.ledvance_plug_compact_eu_em_t_vermogen, sensor.tz3000_okaz9tjs_ts011f_vermogen |
sensor_linear_interp |
sensor.solar_panels_production_power_watts |
sensor.power_load_no_var_loads
This template sensor gives EMHASS the house base load without the deferrable loads. In this setup it starts from total measured consumption (sensor.power_consumed_watts) and subtracts the three controllable loads:
- Peblar EV charger:
sensor.peblar_ev_charger_vermogen - Socket EV charger:
sensor.ledvance_plug_compact_eu_em_t_vermogen - Washing machine:
sensor.tz3000_okaz9tjs_ts011f_vermogen
The result is clamped to 0 so the sensor never goes negative.
This sensor is currently defined through Home Assistant's Template integration (stored in .storage/core.config_entries), not in configuration.yaml.
{{ [
states('sensor.power_consumed_watts') | float(0)
- states('sensor.peblar_ev_charger_vermogen') | float(0)
- states('sensor.ledvance_plug_compact_eu_em_t_vermogen') | float(0)
- states('sensor.tz3000_okaz9tjs_ts011f_vermogen') | float(0),
0
] | max | round(0) }}
Configuration
REST Commands (configuration.yaml)
Two REST commands are defined:
rest_command:
emhass_dayahead_optim:
url: "http://192.168.1.120:5000/action/dayahead-optim"
method: POST
timeout: 120
headers:
Content-Type: application/json
payload: '{"load_cost_forecast": {{ load_cost_forecast }}, "prod_price_forecast": {{ load_cost_forecast }}}'
verify_ssl: false
emhass_publish_data:
url: "http://192.168.1.120:5000/action/publish-data"
method: POST
timeout: 30
headers:
Content-Type: application/json
payload: '{}'
verify_ssl: false
emhass_dayahead_optim— Sends the 24-hour price forecast to EMHASS (both as load cost and production sell price for netting) and triggers the optimizationemhass_publish_data— Publishes the current time step's optimization results back to Home Assistant as sensors
Timeouts are explicitly configured to avoid sporadic failures when optimization takes longer than Home Assistant's default REST timeout:
emhass_dayahead_optim:120secondsemhass_publish_data:30seconds
Helper Entities
The EMHASS flow uses the following Home Assistant helpers:
| Helper Entity | Type | Purpose |
|---|---|---|
input_number.inkoopkosten_per_kwh |
Number | Fixed purchase cost added to each hourly Energyzero spot price |
input_number.energiebelasting_per_kwh |
Number | Energy tax added to each hourly Energyzero spot price |
input_boolean.fingerbot_button |
Boolean | Triggers the Fingerbot press automation for the washing machine |
input_boolean.peblar_ev_charger_manual_override |
Boolean | Prevents EMHASS from turning off the Peblar charger when manually overridden |
input_boolean.wall_ev_charger_manual_override |
Boolean | Prevents EMHASS from turning off the wall/socket charger when manually overridden |
Helper definitions are managed via Home Assistant UI helpers and stored in:
.storage/input_number.storage/input_boolean
Automations
1. EMHASS Daily Optimization
Runs daily at 15:00 — this is when Energyzero publishes next-day prices, giving a full 24-hour forecast window (15:00 today → 15:00 tomorrow).
The automation:
- Fetches energy prices from Energyzero (today + tomorrow in one call)
- Adds fixed cost components (
input_number.inkoopkosten_per_kwh+input_number.energiebelasting_per_kwh) to each hourly price - Filters the next 24 hours of prices and builds a 24-value list starting at the current local hour
- Sends that list to EMHASS for day-ahead optimization
- Triggers the publish/control automation so results are published and deferrable loads are controlled immediately
Energyzero returns timestamps in UTC (+00:00). The automation converts timestamps to local time (as_local(...)) only to filter and sort the next 24 local hours correctly, then sends EMHASS a plain list of price values. EMHASS is configured with method_ts_round: first, so the forecast window starts at the current timestep (floor to the hour), which keeps publish/control decisions available for the current hour. This also avoids EMHASS's timestamp-dictionary alignment path, which was causing the price table in the EMHASS UI to appear shifted by +2 hours with the first value repeated.
- id: '1774121231450'
alias: EMHASS Daily Optimization
description: Run EMHASS day-ahead optimization at 15:00 when next-day prices are available
triggers:
- at: '15:00:00'
trigger: time
actions:
- action: energyzero.get_energy_prices
data:
incl_vat: true
config_entry: 01K7YRH6PWB95CBM9DBBA7RQCY
start: '{{ now() }}'
end: '{{ now() + timedelta(days=1) }}'
response_variable: energy_result
- variables:
load_cost_forecast: >
{% set ns = namespace(prices=[]) %}
{% set current_hour = now().replace(minute=0, second=0, microsecond=0) %}
{% set end_hour = current_hour + timedelta(hours=24) %}
{% set inkoop = states('input_number.inkoopkosten_per_kwh') | float(0) %}
{% set belasting = states('input_number.energiebelasting_per_kwh') | float(0) %}
{% for p in energy_result.prices %}
{% set ts = as_datetime(p.timestamp) %}
{% set ts_local = as_local(ts) %}
{% if ts_local >= current_hour and ts_local < end_hour %}
{% set ns.prices = ns.prices + [{'ts': ts_local, 'value': (p.price + inkoop + belasting) | round(4)}] %}
{% endif %}
{% endfor %}
{{ (ns.prices | sort(attribute='ts') | map(attribute='value') | list)[:24] | tojson }}
- action: rest_command.emhass_dayahead_optim
data:
load_cost_forecast: '{{ load_cost_forecast }}'
- delay:
seconds: 5
- action: automation.trigger
target:
entity_id: automation.emhass_control_deferrable_loads
mode: single
2. EMHASS Publish & Control Deferrable Loads
Runs every 5 minutes. First publishes the latest optimization results, then controls the loads. Running more often than the optimization_time_step (60 minutes) is safe — repeated publish calls within the same hour return the same timestep values — and improves resilience when a sensor or service is briefly unavailable.
Logic per load:
- Turn ON when
sensor.p_deferrable{N}> 500 W and device is currently off - Turn OFF (EV chargers only) when
sensor.p_deferrable{N}< 100 W and device is currently on - Manual override protection: OFF actions for EV chargers only run when corresponding manual override helper is
off - Washing machine safety: only press Fingerbot when
sensor.tz3000_okaz9tjs_ts011f_vermogenis below 10 W (machine not already running)
- id: '1774121231451'
alias: EMHASS Publish & Control Deferrable Loads
description: Publish EMHASS results every 5 minutes and control EV chargers and
washing machine based on optimization
trigger:
- platform: time_pattern
minutes: /5
action:
- action: rest_command.emhass_publish_data
- delay:
seconds: 5
- choose:
- conditions:
- condition: numeric_state
entity_id: sensor.p_deferrable0
above: 500
- condition: state
entity_id: switch.peblar_ev_charger_opladen
state: 'off'
sequence:
- action: switch.turn_on
target:
entity_id: switch.peblar_ev_charger_opladen
- conditions:
- condition: numeric_state
entity_id: sensor.p_deferrable0
below: 100
- condition: state
entity_id: switch.peblar_ev_charger_opladen
state: 'on'
- condition: state
entity_id: input_boolean.peblar_ev_charger_manual_override
state:
- 'off'
sequence:
- action: switch.turn_off
target:
entity_id: switch.peblar_ev_charger_opladen
- conditions:
- condition: numeric_state
entity_id: sensor.p_deferrable1
above: 500
- condition: state
entity_id: switch.ledvance_plug_compact_eu_em_t
state: 'off'
sequence:
- action: switch.turn_on
target:
entity_id: switch.ledvance_plug_compact_eu_em_t
- conditions:
- condition: numeric_state
entity_id: sensor.p_deferrable1
below: 100
- condition: state
entity_id: switch.ledvance_plug_compact_eu_em_t
state: 'on'
- condition: state
entity_id: input_boolean.wall_ev_charger_manual_override
state:
- 'off'
sequence:
- action: switch.turn_off
target:
entity_id: switch.ledvance_plug_compact_eu_em_t
- conditions:
- condition: numeric_state
entity_id: sensor.p_deferrable2
above: 500
- condition: state
entity_id: input_boolean.fingerbot_button
state: 'off'
- condition: numeric_state
entity_id: sensor.tz3000_okaz9tjs_ts011f_vermogen
below: 10
sequence:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.fingerbot_button
mode: single
3. Manual Override for EV Chargers
This automation adds robust manual control for both EV chargers while keeping EMHASS scheduling active.
Key behavior:
- If an EV charger is turned on while EMHASS indicates it should be off (
p_deferrable < 100), the corresponding manual override helper is turned on. - While override is on, the hourly EMHASS control automation is blocked from turning that charger off.
- Toggling an override helper directly turns the matching charger on/off.
- Each override helper auto-resets to
offafter 10 hours.
- id: '1775707532352'
alias: Manual override for EV Chargers
description: This prevents turning OFF the EV chargers by EMHASS when they have been turned on using the override switches, or when they have been turned on while EMHASS did not schedule it.
triggers:
- trigger: state
entity_id:
- switch.peblar_ev_charger_opladen
to:
- 'on'
id: deferrable0-on
- trigger: state
entity_id:
- switch.ledvance_plug_compact_eu_em_t
to:
- 'on'
id: deferrable1-on
- trigger: state
entity_id:
- input_boolean.peblar_ev_charger_manual_override
to:
- 'on'
for:
hours: 10
minutes: 0
seconds: 0
id: reset-deferrable0-manual-override
- trigger: state
entity_id:
- input_boolean.wall_ev_charger_manual_override
to:
- 'on'
for:
hours: 10
minutes: 0
seconds: 0
id: reset-deferrable1-manual-override
- trigger: state
entity_id:
- input_boolean.peblar_ev_charger_manual_override
id: manual-override-deferrable0
- trigger: state
entity_id:
- input_boolean.wall_ev_charger_manual_override
id: manual-override-deferrable1
mode: single
Manual Testing
Test the EMHASS optimization endpoint:
curl -X POST http://192.168.1.120:5000/action/dayahead-optim \
-H "Content-Type: application/json" \
-d '{"load_cost_forecast": [-0.06, -0.04, 0, 0, 0.07, 0.13, 0.16, 0.15, 0.13, 0.12, 0.11, 0.12, 0.15, 0.18, 0.17, 0.17, 0.18, 0.17, 0.08, 0.01, -0.01, -0.04, -0.07, -0.07]}'
Publish the results to Home Assistant:
curl -X POST http://192.168.1.120:5000/action/publish-data \
-H "Content-Type: application/json" \
-d '{}'
Files Modified
configuration.yaml— Addedrest_command.emhass_dayahead_optimandrest_command.emhass_publish_dataautomations.yaml— Added 3 EMHASS automations: daily optimization, hourly publish/control, and EV charger manual override handling.storage/input_number— Contains EMHASS-related price add-on helpers (inkoopkosten_per_kwh,energiebelasting_per_kwh).storage/input_boolean— Contains EMHASS-related control helpers (fingerbot_button, charger manual overrides)