My House Now Decides for Itself When It Uses Power | Forrict Skip to main content
AI Build log

My House Now Decides for Itself When It Uses Power

Fons Biemans
My House Now Decides for Itself When It Uses Power
A build log on how I use Home Assistant, EMHASS and Claude Code to run my home battery, water heater, dishwasher and car at the cheapest moment, every quarter hour. With the real code, the safety nets and the results from September 2026.

My house decides for itself when the car charges, the water heater heats up, the dishwasher runs and the home battery charges or discharges. Every quarter hour it works out again what is cheapest, based on electricity prices, the solar forecast and my calendar. I built it in Home Assistant, together with AI. Below I explain how, with the real code included.

The code is taken straight from my own setup, so entity names and comments are in Dutch.

Why I built this

With the Dutch net metering scheme coming to an end, I wanted to work out what to do with my solar panels. We decided to buy a home battery. The reason: during the day we have a surplus, while our demand for power falls outside the hours the panels produce. With a battery, switching to a dynamic contract also made more sense.

On top of that, our battery can keep the house running during a power cut. That matters more to us now, because the Dutch power grid is getting increasingly congested and, as we see it, less stable.

With a dynamic contract, the electricity price changes every quarter hour, sometimes by a factor of three within a single day. Run your big consumers at the right moment and you save real money. Try that with separate timers and switches and you lose track.

This is the situation in our home (October 2025 through September 2026):

ComponentWhatPer year
House consumptionincluding car and water heater~11,200 kWh
Solar panels34 panels, Fronius Gen24 inverter9,617 kWh
Home batteryFronius battery, 15.8 kWh, since March 20263,825 kWh charged
ContractTibber, price per quarter hour, since March 2026 (switched when we bought the battery)
CarBMW iX1, wallbox up to 22 kW~4,200 kWh
Water heaterDaalderop Mono copper, 120 litres, 1,500 W, via a Shelly relay~120 kWh per month
DishwasherSiemens with Home Connect

The question is the same every day: who gets which kilowatt-hour, and when? Should the battery charge from the grid now because prices go up later? Should the car wait until after midnight? Should the water heater use solar power or a cheap moment from the grid?

Those decisions depend on each other. If the battery and the car both aim for the cheapest quarter hour, you overload your grid connection or the car charges from the home battery. So one planner had to weigh everything at once.

The design in one picture

Home Assistant collects the data and turns every device into three numbers. Every quarter hour, EMHASS turns those into one plan for everything together. Each device has its own controller that carries out the plan, and falls back to a safe default if no plan arrives.

Design: Home Assistant collects the inputs and turns every device into three numbers, EMHASS makes one plan every quarter hour, and a controller per device carries it out

The brain: a new plan every quarter hour

The planner is EMHASS, an open-source optimizer that runs as an app next to Home Assistant. Every quarter hour, EMHASS receives the prices Tibber already knows (at least 24 and at most 48 hours ahead), the solar forecast, the expected power use of the house and the battery’s state of charge. We know that power use from the data of the electricity meter in the meter cupboard. From that, a linear program calculates the cheapest plan for the battery and three “deferrable loads”: water heater, dishwasher and car.

The goal is profit: pay as little as possible for import and earn as much as possible from export. Once net metering ends, that goal will probably shift to using as much of our own power as possible. Recalculating every quarter hour is called model predictive control: if the sun shines differently or the car comes home later, the next plan adapts by itself.

The automation that drives this is short. First it fetches the prices from Tibber, then it sends everything to EMHASS:

- alias: EMHASS - MPC optimalisatie met Tibber prijzen
  triggers:
    - trigger: time_pattern
      minutes: /15
  actions:
    - action: tibber.get_prices
      data:
        start: "{{ now().strftime('%Y-%m-%d') }} 00:00:00"
        end: "{{ (now() + timedelta(days=2)).strftime('%Y-%m-%d') }} 00:00:00"
      response_variable: tibber_response
    - action: rest_command.emhass_mpc_optim
      data:
        load_cost: "..."         # alleen de prijzen vanaf het huidige kwartier
        soc_init: "{{ states('sensor.thuisaccu_soc') | float(0) / 100 }}"
        boiler_timesteps: "{{ states('sensor.boiler_benodigde_kwartieren') }}"
        vaat_timesteps: "{{ states('sensor.vaatwasser_emhass_kwartieren') }}"
        ev_timesteps: "{{ states('sensor.ev_emhass_kwartieren') }}"
        # ... en per apparaat de deadline en het vermogen
    - action: rest_command.emhass_publish_data

The most important design decision is in those last lines. Each device only tells EMHASS how many quarter hours it needs, by when and at what power. Home Assistant calculates those three numbers per device. EMHASS decides which quarter hours it will be. That keeps the planner generic, and the knowledge about each device stays in Home Assistant.

The call to EMHASS itself looks like this:

rest_command:
  emhass_mpc_optim:
    url: "http://emhass:5000/action/naive-mpc-optim"
    method: POST
    payload: >
      {"load_cost_forecast": {{ load_cost }},
       "prod_price_forecast": {{ prod_price }},
       "soc_init": {{ soc_init }},
       "prediction_horizon": {{ prediction_horizon }},
       "number_of_deferrable_loads": 3,
       "nominal_power_of_deferrable_loads": [1500, {{ vaat_power }}, {{ ev_power }}],
       "operating_timesteps_of_each_deferrable_load": [{{ boiler_timesteps }}, {{ vaat_timesteps }}, {{ ev_timesteps }}],
       "end_timesteps_of_each_deferrable_load": [{{ boiler_end_timestep }}, {{ vaat_end_timestep }}, {{ ev_end_timestep }}],
       "treat_deferrable_load_as_semi_cont": [true, true, false],
       "set_deferrable_load_single_constant": [false, {{ vaat_aan }}, false],
       "minimum_power_of_deferrable_loads": [0, 0, {{ ev_min_power }}]}

Three details make the difference:

  • On or off, or variable. The water heater and the dishwasher are semi-continuous: full power or nothing. The car may charge at any power between 4.1 kW (6 amps) and 11 kW. Later this can go up to 22 kW, but first we are building a safeguard so the main fuse doesn’t trip.
  • One block. The dishwasher gets single_constant: one continuous block. You can’t pause a programme halfway through.
  • No double counting. EMHASS predicts the power use of the house from the past, based on the electricity meter. The water heater and the car were part of that history, so they were counted twice: once in the forecast and once as a deferrable load. A separate sensor now takes them out (sensor.power_load_no_var_loads).

EMHASS writes its plan back into Home Assistant as sensors: sensor.p_batt_forecast for the battery and sensor.p_deferrable0 to 2 for the devices. The controllers below react to those.

The home battery: from plan to inverter

The home battery is a Fronius battery on the Fronius Gen24 inverter. Every quarter hour, EMHASS says how many watts the battery should charge or discharge. An automation translates that into the four modes the Fronius knows via Modbus: Auto, Charge from Grid, Discharge to Grid and Block Discharging.

- alias: EMHASS - Accu aansturen via Fronius Modbus
  triggers:
    - trigger: state
      entity_id: sensor.p_batt_forecast
    - trigger: time_pattern
      minutes: /15
    - trigger: state
      entity_id: binary_sensor.ev_laadt
  actions:
    - choose:
        # 1. De auto laadt: de thuisaccu mag niet in de auto leeglopen
        - conditions:
            - condition: state
              entity_id: binary_sensor.ev_laadt
              state: "on"
          sequence:
            - action: select.select_option
              target: { entity_id: select.thuisaccu_storage_control_mode }
              data: { option: Block Discharging }
        # 2. Geen vers plan: terug naar de veilige standaard
        - conditions:
            - condition: template
              value_template: >
                {{ states('sensor.p_batt_forecast') in ['unavailable', 'unknown']
                   or (now() - states.sensor.p_batt_forecast.last_updated).total_seconds() > 300 }}
          sequence:
            - action: select.select_option
              target: { entity_id: select.thuisaccu_storage_control_mode }
              data: { option: Auto }
        # 3. Ontladen naar het net (dure kwartieren)
        - conditions:
            - condition: numeric_state
              entity_id: sensor.p_batt_forecast
              above: 200
            - condition: numeric_state
              entity_id: sensor.thuisaccu_soc
              above: 16
          sequence:
            - action: select.select_option
              target: { entity_id: select.thuisaccu_storage_control_mode }
              data: { option: Discharge to Grid }
            - action: number.set_value
              target: { entity_id: number.thuisaccu_grid_discharge_power }
              data:
                value: "{{ [[states('sensor.p_batt_forecast') | float(0), 6000] | min, 200] | max }}"
        # 4. Laden uit het net (goedkope kwartieren): zelfde patroon, met Charge from Grid
      default:
        - action: select.select_option
          target: { entity_id: select.thuisaccu_storage_control_mode }
          data: { option: Auto }

The order of the branches is the design. First protect the car, then safety, and only then money. If EMHASS goes down, the battery is back on Auto within five minutes and the inverter simply does self-consumption. Nothing gets stuck on “discharge to grid”.

The EMHASS configuration holds the battery limits: charge between 15% and 90%, and a wear cost of 2 cents per kWh. That last one stops the battery from doing a full cycle for a price difference of a few cents. In the simulation it almost halved the number of cycles, at a cost of 2 to 3 euros per month.

The water heater: a 120-litre battery

A water heater is really a heat battery. It doesn’t matter when it heats up, as long as there is hot water in the morning. Mine is a “dumb” 120-litre Daalderop Mono copper tank, with no smart features of its own. All the intelligence sits outside it: a Shelly 1PM switches the 1,500 W element, and a temperature sensor at the bottom of the tank measures how hot the water is.

Home Assistant calculates how many quarter hours of heating are needed before the next deadline. At night that is 65 °C by 6 in the morning, so there is hot water for showers. During the day it is 75 °C for the evening, because that is when the kids have a bath:

- name: "Boiler benodigde kwartieren"
  state: >-
    {% set temp = states('sensor.boiler_temperatuur') | float(none) %}
    {% set doel = states('sensor.boiler_plan_doel') | float(65) %}
    {% set rate = states('input_number.boiler_opwarmsnelheid') | float(10) %}   {# K per uur #}
    {% set maxk = states('sensor.boiler_kwartieren_tot_deadline') | int(16) %}
    {% set laden = is_state('switch.boiler', 'on') %}
    {% if temp is none %}0
    {% else %}
      {% set delta = doel - temp %}
      {# dode zone van 3 K: niet voor elke tiende graad een kwartier plannen #}
      {% if delta <= 0 or (delta < 3 and not laden) %}0
      {% else %}{{ [((delta / (rate / 4)) | round(0, 'ceil') | int), maxk] | min }}{% endif %}
    {% endif %}

EMHASS picks the quarter hours. But EMHASS doesn’t switch the relay. That is done by a single controller that decides every five minutes which mode applies, in a fixed order:

nieuwe_modus: >-
  {% if not sensor_ok %}sensor_fout
  {% elif modus == 'handmatig' and handmatig_sec < 10800 %}handmatig
  {% elif temp < floor or (modus == 'safety' and temp < recovery_eff) %}safety
  {% elif not enabled %}uit
  {% elif leg_due and (modus == 'legionella' or prijs < prijs_gem or leg_uren_over > 24) %}legionella
  {% elif holiday %}uit
  {% elif negatief and temp < boost - 3 %}boost
  {% elif temp < dfloor - 3 and kw_tot_deadline <= kw_tot_floor + 1 %}deadline
  {% elif emhass_vraagt and temp < plan_doel %}emhass
  {% else %}uit{% endif %}

This is what I call a level-based controller. It doesn’t react to events (“switch on at 3:15”), but looks at the state every five minutes and puts the relay where it belongs. After a restart or flaky Wi-Fi, it recovers by itself within five minutes. We want to make this smarter still.

The safety nets rank above money:

  • Safety: below 50 °C, always heat. If power is expensive at that moment, only up to 55 °C. EMHASS schedules the rest cheaply.
  • Deadline: if EMHASS is about to miss the morning, the controller steps in itself.
  • Legionella: at least half an hour above 60 °C every week, preferably at a cheap moment, at most one day late.
  • Negative price: heat up to 85 °C. You get paid to make hot water.
  • On the Shelly itself: a small script guards the minimum temperature and the legionella cycle, even if Home Assistant is completely down.

In the calculations, smart heating saves about 25% compared to heating at the average daily price.

The dishwasher: you load it, the house picks the moment

Nobody has to open an app for the dishwasher. You load it, choose a programme, press Remote start and close the door. The rest happens by itself:

  1. Home Assistant sees through Home Connect that the machine is ready and sets a deadline. Pressed during the day: clean by 17:00. Pressed in the evening: clean by 6:00.
  2. The chosen programme determines how long it runs and how much power it uses on average. Eco 50 takes 16 quarter hours at ~230 W, Quick 45 two quarter hours at ~1,400 W.
  3. A new EMHASS plan runs immediately, and the family gets a notification: “Scheduled at the cheapest moment: starts at 02:45.”
  4. As soon as EMHASS schedules the dishwasher, Home Assistant starts the programme.

Choosing the deadline is the cleverest bit. If the programme no longer fits before the next deadline, it moves on to the one after. The dishes won’t be ready for dinner anyway, so it might as well run at the cheapest moment:

deadline: >-
  {% set duur = timedelta(minutes=15 * states('sensor.vaatwasser_benodigde_kwartieren') | int(12)) %}
  {% set ochtend = today_at(states('input_datetime.vaatwasser_klaar_ochtend')) %}
  {% set middag = today_at(states('input_datetime.vaatwasser_klaar_middag')) %}
  {% set ns = namespace(d=none) %}
  {% for d in [ochtend, middag, ochtend + timedelta(days=1), middag + timedelta(days=1)] %}
    {% if ns.d is none and d - now() >= duur %}{% set ns.d = d %}{% endif %}
  {% endfor %}
  {{ ns.d.strftime('%Y-%m-%d %H:%M:%S') }}

Starting has two triggers: the moment EMHASS picks, and a safety net at “deadline minus programme duration”. Even if EMHASS fails, the dishes are clean on time.

- alias: Vaatwasser - Starten op EMHASS-moment
  triggers:
    - trigger: state
      entity_id: binary_sensor.vaatwasser_emhass_vraagt
      to: "on"
      id: emhass
    - trigger: template
      value_template: >-
        {% set u = states('sensor.vaatwasser_uiterlijke_start') | as_datetime %}
        {{ u is not none and now() >= u }}
      id: vangnet
  conditions:
    - condition: state
      entity_id: input_boolean.vaatwasser_wacht_op_emhass
      state: "on"
  actions:
    - action: home_connect.start_selected_program
    - action: input_boolean.turn_off
      target: { entity_id: input_boolean.vaatwasser_wacht_op_emhass }

One edge case surprised us. When a programme finishes, the status jumps from finished to ready, with the door closed and Remote start still on. To Home Assistant that looks exactly like a new request, so the dishwasher scheduled itself again. The fix: a new request only counts once the door has been opened after the previous programme. You only learn that kind of behaviour by running it in your own home.

The car: from calendar to charging current

The car is the biggest consumer you can shift: a 64 kWh battery, 11 kW charging. The question isn’t just when to charge, but also how much. For that, the house looks at my calendar.

Step 1: how far do I drive tomorrow?

Every evening at 19:55, a script reads tomorrow’s calendars:

  • the work location from my work calendar (office, a client, or home)
  • meetings in a physical room, which make me leave earlier or get home later
  • the shared family calendar.

For each destination, Home Assistant asks Waze for the distance and travel time, times two for the way back. The first appointment sets the departure time.

- action: waze_travel_time.get_travel_times
  data:
    origin: zone.home
    destination: "{{ bestemming }}"
    region: eu
  response_variable: reis

At 20:00 I get a notification: “Tomorrow 56 km, leave 07:30, charge to 71%.” With buttons to confirm, adjust (type 120 06:45) or charge to full anyway.

Step 2: how much needs to go in?

The base follows from the trip, with a reserve on arrival, a margin that grows with distance, and a fixed buffer of 100 km in case something comes up:

- name: "EV basis SoC"
  unit_of_measurement: "%"
  state: >-
    {% set km = states('input_number.ev_km_morgen') | float(0) %}
    {% set soc_per_km = states('input_number.ev_verbruik_kwh_km') | float(0.2)
                        / states('input_number.ev_accu_kwh') | float(64.7) * 100 %}
    {% set rit_soc = km * soc_per_km %}
    {% set buffer_soc = states('input_number.ev_buffer_km') | float(0) * soc_per_km %}
    {% set rit_doel = states('input_number.ev_aankomst_reserve_soc') | float(20)
                      + rit_soc * (1 + states('input_number.ev_marge_pct') | float(15) / 100)
                      + buffer_soc %}
    {{ [[states('input_number.ev_ondergrens_soc') | float(30), rit_doel] | max, 100] | min | round(1) }}

On top of that comes the price. If power before departure is cheap enough (below 15 cents), the target becomes 80%. If it goes negative at some point, 100%. A final check compares that with tomorrow: if the sun promises free power after I get home, it only charges the base tonight.

Step 3: EMHASS plans, Home Assistant executes

The required energy goes to EMHASS as the third deferrable load, with the departure time as the deadline. The plan comes back as power in watts. A charge controller turns that into amps for the wallbox, in a clear order:

- name: "EV modus"
  state: >-
    {% if is_state('input_boolean.ev_automatisering', 'off') %}uit
    {% elif is_state('binary_sensor.ev_stekker', 'off') %}geen_stekker
    {% elif is_state('input_boolean.ev_geforceerd_laden', 'on') %}geforceerd
    {% elif soc >= states('sensor.ev_doel_soc') | float(100) %}doel_bereikt
    {% elif not emhass_vers or not soc_vers %}failsafe
    {% else %}emhass{% endif %}
  attributes:
    stroom: >-
      {% set f = 3 * 230 %}
      {{ [(states('sensor.p_deferrable2') | float(0) / f) | round(0, 'floor') | int, 16] | min }}

The failsafe exists because the car’s state of charge comes in through BMW’s cloud, which sometimes goes quiet for hours. Is the EMHASS plan older than 30 minutes, or is the state of charge stale while the car is charging? Then the controller charges up to the 30% floor by itself, in the cheapest remaining quarter hours. The car is never left empty on the driveway.

Every decision is logged with a reason, for example: “EMHASS plans 7,360 W now (charge 48% → target 71%, price 12.4 ct).” For a system that makes its own decisions, that is essential: afterwards you need to be able to see why it did something. It also lets us build on this in the future and make it smarter still.

Building with AI: what Claude did and what I did

I didn’t type this line by line myself. I built it together with Claude Code, an AI assistant that works directly in the configuration folder of my Home Assistant. The difference with a chatbot you copy code from: Claude can read the data itself, edit files, check that the configuration is valid, and look at the result in the real system.

What Claude did:

  • Measure before building. Through the connection with Home Assistant, Claude read a year of measurements: grid, solar, battery, water heater, prices. It recalculated my Tibber bill to within 3% of the invoice, and then simulated scenarios: what if there were no battery, what if the water heater and the car ran in the cheapest hours? That way we knew where the money was before there was a single line of code.
  • The work itself. The packages for the water heater, dishwasher and car, the EMHASS integration, the safety nets and the charging dashboard. Each package with a README explaining how it works and why it was built that way.
  • Debugging in the real house. Why did the battery charge the car? Why did the dishwasher schedule itself again? Why did EMHASS count the water heater twice? Claude worked it out from the sensor history, not by guessing.
  • Cleaning up. An audit of the whole configuration found broken automations, unused sensors and passwords that were in git.

What I did:

  • Decide what the house should do and what matters: hot water comes before money, the car must never be empty.
  • Test it in daily life, and come back with “this isn’t right”.
  • Make the decisions you can’t undo.

What I learned about working with an AI agent:

  1. Give it real data. The best decisions came from analysing a year of measurements, not from assumptions.
  2. Demand explanations in the system itself. Every decision in the logbook and an explanation attribute on every target value. Then you can check it, and you trust it.
  3. Small steps, with a safety net. A git commit after every change, and a fallback for when something fails.
  4. Keep irreversible actions with the human. When Claude wanted to delete unused sensors, a few deletions were held back to ask for confirmation. That’s how it should be.
  5. Let it document. In three months I won’t remember why the dead band is 3 degrees. The README will.

This is also what I do for a living. At Forrict I train teams to work with AI agents such as Claude, and I help small and mid-sized businesses automate their own workflows: with real data, small steps, safety nets and explanations built into the system, just like above. Read more about AI with Claude.

Results

In September 2026 we paid € 76.50 for electricity (energy component only, excluding fixed charges and grid fees). Without smart control and the battery, the calculation says that would have been € 142. Before all this, we paid around € 200 a month on a fixed contract. The model reproduces the actual bill with less than 3% deviation. The battery control has been running since March 2026, when the home battery arrived. In one month we even got € 100 back.

Scenario, September 2026Electricity cost
No home battery€ 142.00
Battery on the inverter’s default mode (self-consumption only)€ 119.20
Actual: EMHASS controls the battery€ 76.50
Theoretical optimum with the same battery limits (perfect forecast)€ 71.10
Optimum, plus water heater and car in the cheapest hours€ 46.80

Three things stand out:

  • The battery pays for itself by trading, not by storing solar power. Tibber pays for feed-in per quarter hour at almost the purchase price. The gain is in the price difference between hours: in September, EMHASS bought in at an average of 27.5 cents and fed back at 40.5 cents.
  • The planning is within € 5 of the theoretical maximum. With a perfect forecast of sun and consumption you would barely get more.
  • The next gain is in the car. The last row of the table shows what’s still on the table if the car and the water heater also follow the price. That is exactly what has been built since. Those numbers will follow.

Over the whole year (October 2025 through September 2026) the house used a net ~11,200 kWh. 35% of that came from our own solar and battery, and we used 44% of our solar power ourselves.

Lessons and what I would do differently

  • One planner, many executors. EMHASS calculates, and per device a simple controller with fixed priorities decides. If the planner goes down, everything keeps running safely.
  • Safety nets above money. Hot water, a car that’s never empty and a battery that doesn’t get stuck on “discharge” matter more than a few cents.
  • Make it explainable. A system that makes its own decisions must be able to tell you why. Otherwise you’ll switch it off within a month.
  • Watch the rules that change. From 1 January 2027 the Dutch net metering scheme (salderingsregeling) ends. Feeding back will earn less and using it yourself will be worth more. Because the logic lives in one planner, I mostly adjust the prices and a few settings instead of rebuilding everything.

Why not just use evcc? evcc is an excellent open-source project for smart charging and follows solar surplus faster than my solution. But evcc only controls the car. It knows nothing about my calendar, and doesn’t plan the car together with the battery, the water heater and the dishwasher. For me, the gain is in that whole.

Finally

This project started as a hobby, but the pattern is broader than a house. Bringing together data from different systems, an optimization that decides again every few minutes, safety nets for when something fails, and an AI agent that builds, tests and documents it. Importantly, the process itself is deterministic: given the same data, it always makes the same decision. That works just as well for other processes, such as energy management in a business or AI workflows that process invoices.

Want this kind of automation in your organisation too? I train teams who want to start working with AI, and help small and mid-sized businesses automate their workflows. Or do you just want to see the full code? Send me a message. I’m happy to think along.

Sources

F

Fons Biemans

AWS expert and consultant at Forrict, specializing in cloud architecture and AWS best practices for Dutch businesses.

Tags

Home Assistant EMHASS Dynamic energy contract Home battery Claude Code

Stay in the loop

Get AWS insights and tips delivered to your inbox.

Stay in the loop

Get AWS insights and tips delivered to your inbox.