Projects   ›   Home Assistant   ›   EMHASS

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 optimization
  • emhass_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: 120 seconds
  • emhass_publish_data: 30 seconds

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:

  1. Fetches energy prices from Energyzero (today + tomorrow in one call)
  2. Adds fixed cost components (input_number.inkoopkosten_per_kwh + input_number.energiebelasting_per_kwh) to each hourly price
  3. Filters the next 24 hours of prices and builds a 24-value list starting at the current local hour
  4. Sends that list to EMHASS for day-ahead optimization
  5. 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_vermogen is 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:

  1. 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.
  2. While override is on, the hourly EMHASS control automation is blocked from turning that charger off.
  3. Toggling an override helper directly turns the matching charger on/off.
  4. Each override helper auto-resets to off after 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 — Added rest_command.emhass_dayahead_optim and rest_command.emhass_publish_data
  • automations.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)