Heat Pump Cost Optimizer

Documentation

Automations

Without a heat pump on/off switch — and without the ECL110 or frequency-control paths — the optimizer's plan is published on sensors for your own automations to act on (configuration.md says exactly that where the trade-off is made). The examples below are complete and use only entities and services this integration actually creates. Entity ids are fixed at sensor.heat_pump_optimizer_* regardless of the name you gave the entry, so they read sensor.heat_pump_optimizer_cost_power_headroom on every install.

Automation example: charge an EV from the Cost Power Headroom sensor

The Cost Power Headroom sensor (sensor.heat_pump_optimizer_cost_power_headroom) is min(main fuse, capacity threshold) − current house draw, clamped at zero, in kW — a number an EV charger can follow. It stays unavailable only while nothing bounds the house: set a main fuse size in the options, or enable a capacity tariff, and it appears — the fuse is unset by default, so a tariff-only install has the sensor too. Until the month's first metering window closes there is no reference peak yet, so a tariff-only install then reads 0.0 kW — no kW is free while the month's peak is being set — and the limit_source attribute reads capacity tariff with no peak reference yet, which tells that state apart from a measured limit. With no fuse and no capacity tariff it stays unavailable, and without a whole-house meter it only sees the heat pump itself, which the sensor's attributes say out loud.

This automation starts a simple charger whenever at least 5 kW of headroom opens up and stops it below 2 kW:

automation:
  - alias: Charge the EV while the grid has headroom
    trigger:
      - platform: numeric_state
        entity_id: sensor.heat_pump_optimizer_cost_power_headroom
        above: 5
      - platform: numeric_state
        entity_id: sensor.heat_pump_optimizer_cost_power_headroom
        below: 2
    action:
      - choose:
          - conditions:
              - condition: numeric_state
                entity_id: sensor.heat_pump_optimizer_cost_power_headroom
                above: 2
            sequence:
              - service: switch.turn_on
                target:
                  entity_id: switch.ev_charger
        default:
          - service: switch.turn_off
            target:
              entity_id: switch.ev_charger

A charger with a dynamic current limit can instead follow the number directly, writing it to the charger's current entity with number.set_value.

Automation example: economy mode when electricity is expensive

The Cost Electricity Price (now) sensor (sensor.heat_pump_optimizer_cost_current_electricity_price) publishes the hourly spot price in your currency per kWh. set_mode accepts auto, comfort, economy, boost and off: economy lets the plan ride out expensive hours up to 1.5 °C below the comfort floor (never below 15 °C), and auto hands full optimization back. Swap the thresholds for your market's prices:

automation:
  - alias: Economy mode through the evening price peak
    trigger:
      - platform: numeric_state
        entity_id: sensor.heat_pump_optimizer_cost_current_electricity_price
        above: 0.40
        id: price_high
      - platform: numeric_state
        entity_id: sensor.heat_pump_optimizer_cost_current_electricity_price
        below: 0.25
        id: price_back_down
    action:
      - choose:
          - conditions:
              - condition: trigger
                id: price_high
            sequence:
              - service: heatpump_optimizer.set_mode
                data:
                  mode: economy
          - conditions:
              - condition: trigger
                id: price_back_down
            sequence:
              - service: heatpump_optimizer.set_mode
                data:
                  mode: auto

set_mode acts on every loaded entry at once and requests a fresh solve immediately, so the new mode shows up in the plan straight away. The services stay registered while every entry is unloaded; a call that then finds no loaded entry fails with a validation error rather than doing nothing (see configuration.md).

Automation example: know when a manual plan is pinned

While an apply_manual_plan override is active, the plan sensors carry a manual_override attribute with the expiry time; the attribute is absent otherwise. This fires a notification when one appears:

automation:
  - alias: Notify when a manual plan takes over
    trigger:
      - platform: state
        entity_id: sensor.heat_pump_optimizer_plan_space_heating
        attribute: manual_override
    condition:
      - "{{ trigger.to_state.attributes.get('manual_override') is not none }}"
    action:
      - service: notify.persistent_notification
        data:
          title: Manual plan active
          message: >-
            Heating slots are pinned until
            {{ state_attr('sensor.heat_pump_optimizer_plan_space_heating',
            'manual_override').expires_at }}.

The pins constrain timing only — safety still releases any slot the tank minimum, the legionella clock or the comfort floor cannot honour, and released slots are reported in the same manual_override attribute.

Events: what the optimizer tells you

The integration fires five Home Assistant events, each once per occurrence, so an automation decides where a message goes. Every event carries entry_id, the config entry that fired it. What has been sent is remembered across restarts, so a condition that is still true after a restart is not announced again, and an unchanged refresh fires nothing. A condition that clears and comes back fires again. That includes a manual plan whose released slots lapse for one solve and return, which fires again, and a plan that crosses your minimum temperature and back on successive refreshes, which fires at each crossing.

Event Fires when Data
heatpump_optimizer_monthly_receipt a month has closed and its receipt is published entry_id, month (YYYY-MM), total_sek, saving_sek and saving_pct (against a plain thermostat with no wood, null when the month has no savings row, was booked on an older reference, or is under the meter floor), currency, saving_old_basis (true for that older reference), saving_includes_wood (true where wood is configured: the saving includes its heat, and firewood's cost is not counted), saving_below_meter_floor (true when the month's baseline used under half the energy the meter recorded, so no saving is stated)
heatpump_optimizer_comfort_at_risk the coldest step of the plan is below your minimum temperature entry_id, predicted_min_c, at (the step's time), floor_c, peak_guard_suppressing (the peak guard is holding heating back), cause (the coldest step's published reason, for example idle_fuse, or null when the plan has none)
heatpump_optimizer_input_stale a required input has not updated within its limit; once per input entry_id, input, age_minutes, max_age_minutes
heatpump_optimizer_plan_stale the plan is older than three solve cycles (at least 90 minutes) entry_id, age_minutes
heatpump_optimizer_manual_plan_released safety released slots of a manual plan; once per channel for each release entry_id, channel (space or dhw), steps (the released step numbers when it fired), reason, expires_at

A receipt that already exists when the integration is updated to the release that adds these events is not announced; the next month's is.

automation:
  - alias: Tell me when the house may get cold
    trigger:
      - platform: event
        event_type: heatpump_optimizer_comfort_at_risk
    action:
      - action: notify.mobile_app_phone
        data:
          message: >-
            The plan expects {{ trigger.event.data.predicted_min_c }} °C,
            below your {{ trigger.event.data.floor_c }} °C minimum.

The blueprint below turns all five into messages for one notify target.

Blueprints

The three examples above, and the events, are also shipped as importable Home Assistant blueprints, taking the sensor and thresholds as inputs instead of the hard-coded entity ids. Import them from Settings → Automations & Scenes → Blueprints → Import Blueprint:

Rendered from docs/automations.md, which is the source of truth; this page changes only when that file does.