Your heat pump on its own line: what it costs, then automate in HA

  • Tested on firmware v1.1.4
  • Intermediate

Part 3 of 3 · Home Assistant recipes

A heat pump is usually the largest single load in a house, and the one whose cost is hardest to guess. It runs when the weather says so, not when you do, and on a day/night tariff that means its bill depends on hours you never chose. Measuring it on its own circuit answers the first question — what does it actually cost — and once Home Assistant knows which tariff zone the hub is in, you can do something about the second.

What you need:

  • an RBgrid hub with the heat pump's circuit measured as its own point;
  • a tariff plan entered on the hub — if you haven't done that, start with your own time-of-use tariff;
  • the hub set to your time zone, with its clock synchronized: every zone boundary in this guide is resolved against the hub's clock, not yours (which time zone should the hub use);
  • an MQTT broker, with Home Assistant connected to the hub. This guide starts after that is working. If it isn't yet, A power meter for Home Assistant sets it up step by step — the broker, the hub's Settings → Home Assistant form, and the first entities — and then come back here.

For the wiring and the clamp sizing, the three-phase build in Monitoring a three-phase house covers a heat pump end to end.

Safety. For any work inside an electrical panel, call a qualified electrician.

1. Give the heat pump its own point

In Management → Points, bind the heat pump's channel and set its Role to Heating/cooling. The role is what tells the app this is a load with its own schedule: the standby-load card, for one, takes heating and EV-charging lines out of the house's baseline before it estimates what's always on, because a pump that runs at 03:00 isn't standby.

Name the point before it ever reaches Home Assistant. This matters more than it looks — see §3.

Single-phase pumps are one point. A three-phase pump is one clamp per conductor, each on the Basic module of that phase, bound as three points and grouped; the three-phase guide walks through it.

2. What the app shows you

Monitoring → Economics, with the heat pump selected, gives its energy and its cost for an hour, today, yesterday, the last 7 days, this month, last month or this year. The cost is the one the hub computed as the energy was used: every 15 minutes, at the price of the zone in effect then, stored with the hourly record. Nothing is re-priced later, so when your supplier changes a rate, what the pump cost last winter stays what it cost.

History in the Money metric puts it next to your other circuits over the same period, and Download CSV takes the numbers with you.

On the cost by zone: Economics has that split, but read it for what it is — a running total since the hub last restarted, which doesn't follow the period selector. It answers "how is the split looking right now", not "what did the pump cost me in the night zone in October". There's no per-month view of cost by zone; for a month, use the period selector and take the total.

And before you reach for the CSV to do it yourself: the stored records carry energy and money per period but no zone field — the split lives only in that running total. So night-versus-day for a month isn't a view you're missing, it's a figure the hub doesn't keep.

3. Send it to Home Assistant

With MQTT connected, each bound point appears as its own device with eight sensors: current, power, reactive power, power factor, and energy in and out as both lifetime and day counters. Energy goes into the Energy dashboard under Individual devices directly — no utility_meter, no template sensor. Use the lifetime Energy In counter, not the (Day) one: the day counters reset at the hub's local midnight, while the lifetime totals are guaranteed never to decrease and survive a reboot or a firmware update in the hub's own memory. A factory reset clears them, and so does deleting the point.

Two things that will save you an evening:

  • The entity id is frozen at first discovery. Rename the point in the app afterwards and only the display name changes: a point first discovered as "UI7 I2" keeps sensor.rbgrid_ui7_i2_power. You can rename the entities in Home Assistant, so it isn't permanent — it's just work you can avoid. Name it before you connect the broker, or rename the entities in HA once.
  • The role doesn't travel. Heating/cooling is an app concept; in Home Assistant the heat pump's device looks exactly like every other point. Whatever you want to do with the role, do it on the RBgrid side.

4. Make the tariff zone visible to HA

The hub publishes its current tariff zone, but not as a discovered entity — discovery covers the measurements, not the tariff. The zone lives in the hub's telemetry frame, which is published every three seconds and is not retained. One MQTT sensor picks it up. <MAC> is the hub's twelve hex characters in lower case — topics are case-sensitive, and the rbgrid/ prefix is fixed:

yaml
mqtt:
  sensor:
    - name: "Tariff Zone"
      unique_id: ha_rbgrid_<MAC>_tariff_zone
      default_entity_id: sensor.rbgrid_tariff_zone
      state_topic: "rbgrid/<MAC>/telemetry"
      value_template: >-
        {% set t = value_json.get('system', {}).get('tariff') or {} %}
        {{ t.zone if t.zone is defined else None }}
      expire_after: 30
      availability:
        - topic: "rbgrid/<MAC>/status"
          payload_available: "online"
          payload_not_available: "offline"
      json_attributes_topic: "rbgrid/<MAC>/telemetry"
      json_attributes_template: >-
        {% set t = value_json.get('system', {}).get('tariff') or {} %}
        {{ {'plan_active': t.zone is defined,
            'time_src': value_json.get('time_src'),
            'buy_rate': t.get('buy_rate'), 'sell_rate': t.get('sell_rate'),
            'currency': t.get('currency'),
            'placeholder': t.get('placeholder', false)} | tojson }}
      device:
        identifiers: ["<MAC>"]
    - name: "Tariff Buy Rate"
      unique_id: ha_rbgrid_<MAC>_tariff_buy_rate
      default_entity_id: sensor.rbgrid_tariff_buy_rate
      state_topic: "rbgrid/<MAC>/telemetry"
      value_template: >-
        {% set t = value_json.get('system', {}).get('tariff') or {} %}
        {{ t.buy_rate if t.buy_rate is defined else None }}
      unit_of_measurement: "EUR/kWh"
      expire_after: 30
      availability:
        - topic: "rbgrid/<MAC>/status"
          payload_available: "online"
          payload_not_available: "offline"
      device:
        identifiers: ["<MAC>"]

Three details in there are not decoration.

The templates are guarded, and they have to be. The tariff fields are absent from the frame until a plan exists on the hub — and connecting the broker before entering a tariff is a perfectly normal order of work. An unguarded value_json.system.tariff.zone throws on the missing key, and Home Assistant logs nothing: MQTT swallows template errors quietly. Worse, the sensor re-arms expire_after before it evaluates the template, so a hub with no plan keeps the sensor alive on its last value indefinitely — a stale zone that looks perfectly healthy. The guarded version reports unknown instead, which is a state your automation can refuse to act on.

The second sensor is the current price, and it's there for a reason worth spelling out. Home Assistant's Energy dashboard can take an entity as the current price for a source, so the hub's buy_rate gives your dashboard the right number for the zone you're in right now — which is how cost gets computed on the Home Assistant side, with your own tariff, for whatever you already have there. RBgrid keeps the per-circuit money; HA keeps the whole-house picture it has always had.

plan_active is your only tell. Since nothing appears in the log, that attribute is how you see whether the hub actually has a tariff. Worth putting on a dashboard if you intend to rely on any of this.

default_entity_id pins the entity id. MQTT entities take their id from the device name plus the entity name, so without it this sensor lands at sensor.rbgrid_system_tariff_zone and every automation below quietly watches nothing. (object_id did the same job and is deprecated from HA 2025.12 onwards.) The ha_ prefix on unique_id keeps it clear of the firmware's own namespace, so if a future release discovers a zone entity itself, the two coexist and you delete this block.

expire_after: 30 still matters: the frame isn't retained, so if the hub goes quiet the sensor turns unavailable rather than holding a zone that may no longer be true.

The availability topic is doing real work too: the hub publishes online retained on every connect, and registers an MQTT last will so the broker publishes offline if it drops without saying goodbye. A Home Assistant that restarts therefore learns the hub's state immediately rather than waiting for a frame. And if your broker restarts and loses its retained messages, the hub re-publishes its whole discovery set on reconnect, so your point entities aren't orphaned.

One trap before you build on this. time_src reads sntp only after the hub has reached an NTP server. A clock you set by hand from your browser still reports none, even though it's correct — so on a hub with no internet access, the condition in the next section never passes and the automation never fires, silently and forever. If time_src stays none, that's what to chase: the hub's route to a time server, not the automation.

The sensor reads one of eight values — NIGHT, MORNING_PEAK, DAY, EVENING_PEAK, LATE_EVENING, WEEKEND, HOLIDAY, CRITICAL_PEAK — in that spelling. It's the active zone's type, not the name you gave it, so two zones of different types at the same price still read differently. With no tariff plan on the hub the field isn't in the frame at all, and the guarded sensor reads unknown with plan_active: false.

You'll know it works when Developer Tools → States shows the new sensor reading a zone name such as NIGHT, with plan_active: true, time_src: sntp and placeholder: false among its attributes. If it sits at unknown, the topic or the template path is wrong for your hub: subscribe to rbgrid/# in any MQTT client and compare a real frame against the template.

The attributes come along for a reason, and it's a sharper one than it looks. The hub works out the zone from its own clock even when that clock hasn't been synchronized yet — after a boot without SNTP it sits near 1970 plus uptime, so the zone it reports, and the price it charges, can be simply wrong. It doesn't withhold the field while it waits. time_src is how you tell, and the next section refuses to act until it reads sntp.

⚠️ After any restart, re-save your tariff before trusting these figures — or letting them drive anything. On v1.1.4 a hub that reboots comes back on its built-in placeholder plan Test Day/Night until you open Tariffs and press Save (the tariff guide has the detail). The catch for this article: while that's true, the zone still reads a perfectly plausible NIGHT and time_src still reads sntp, so the check in the next section passes and your automation drives the heat pump off the test plan's boundaries. A power cut at 02:00 is the case to picture.

5. Automate — in Home Assistant, on the setpoint

RBgrid measures and reports. It switches nothing: no relay, no contactor, no output of any kind. Everything below runs on the Home Assistant side, against a climate entity you already have.

yaml
automation:
  - alias: "Heat pump: follow tariff zone (reconcile)"
    mode: single
    max_exceeded: silent
    triggers:
      - trigger: state
        entity_id: sensor.rbgrid_tariff_zone
        to: ~
      - trigger: state
        entity_id: sensor.rbgrid_tariff_zone
        attribute: placeholder
      - trigger: homeassistant
        event: start
      - trigger: time_pattern
        minutes: "/15"
    conditions:
      - condition: state
        entity_id: sensor.rbgrid_tariff_zone
        state: ["NIGHT", "DAY"]
      - condition: template
        value_template: "{{ state_attr('sensor.rbgrid_tariff_zone', 'time_src') == 'sntp' }}"
      - condition: template
        value_template: "{{ state_attr('sensor.rbgrid_tariff_zone', 'placeholder') is sameas false }}"
    actions:
      - variables:
          target: "{{ 21 if is_state('sensor.rbgrid_tariff_zone', 'NIGHT') else 19 }}"
      - condition: template
        value_template: "{{ (state_attr('climate.heat_pump', 'temperature') | float(0)) != (target | float) }}"
      - action: climate.set_temperature
        target: { entity_id: climate.heat_pump }
        data: { temperature: "{{ target }}" }
  - alias: "RBGrid: placeholder tariff plan active"
    mode: single
    triggers:
      - trigger: state
        entity_id: sensor.rbgrid_tariff_zone
        attribute: placeholder
        to: true
    actions:
      - action: persistent_notification.create
        data:
          notification_id: rbgrid_placeholder_plan
          title: "RBGrid: placeholder tariff plan"
          message: >-
            The hub is running the built-in Test Day/Night plan. Re-save your tariff
            plan in the RBgrid app; zone automations stay paused until then.

The placeholder guard is the third condition, and it's the one most people won't think of. A hub that reboots on v1.1.4 comes back on its built-in Test Day/Night plan until you re-save yours — and while that's true, the zone still reads plausibly and the clock still reads synced, so the obvious checks all pass. Acting on it would drive your heat pump off test prices.

The hub says so, though, if you ask: while the built-in plan is active the telemetry carries placeholder: true, and the key simply isn't there once your own plan is saved. The condition is strict on purpose — it acts only on an explicit false, never on a missing attribute — so a sensor that can't tell you is treated the same as a hub that's on the test plan.

The second automation does nothing but tell you why everything has gone quiet. And because the reconcile automation also triggers on that attribute, the moment you re-save your plan in the app the setpoint catches up at once rather than waiting for the next quarter-hour.

Point this at a room thermostat, and check it first. climate.set_temperature assumes an entity with a settable room target in °C. Many heat-pump integrations expose something else: a flow or water temperature, a weather-compensation curve where a target does nothing, a preset-only entity with no settable temperature — or the hot-water tank. Setting 21 on a flow-temperature entity is not a nudge; on a DHW tank it's a cold cylinder and a legionella cycle you didn't intend.

Open Developer Tools → States, find your climate entity, and confirm it has a temperature attribute in the range you expect before you let anything write to it.

Pair it with the mirror automation on the day zone, setting the temperature one or two degrees lower, or climate.set_preset_mode: eco if your unit has one.

Check that NIGHT is really your cheap zone. The sensor reports the zone's type, not the name you gave it, and the type decides what happens:

  • If your plan has a Weekend zone, it outranks Night on the days it runs — so this automation does nothing at the weekend, when the house is occupied all day. That's usually the opposite of what you want.
  • If your cheap rate comes in two windows — a cheap night and a cheap midday, which suits a heat pump well — the second one arrives as a different type, such as LATE_EVENING, and needs its own trigger. Two cheap windows in one day explains how such a plan is built.
  • On a three-zone tariff, "the day zone" might be DAY, MORNING_PEAK or EVENING_PEAK, and you'll want a trigger for each.

Watch the sensor for a day before you trust a single to: value.

Turn off whatever schedule you already run on this entity first. If a time-based automation and this one both write the setpoint, they will fight, and the one that wrote last wins — which is not a property you want deciding your heating.

The hub re-resolves the zone every three seconds and publishes on its own three-second timer, so a zone change shows up in about six seconds at worst, usually sooner, plus Home Assistant's own latency. Fast enough for a setpoint; don't build anything that needs the boundary to the second.

It converges; it doesn't react. This is the part worth understanding before you adapt it. An automation that only fires on the transition into a zone has a failure you won't notice: if Home Assistant is restarting, or the broker blips, or the hub is quiet when 23:00 passes, the transition is missed and the setpoint is never restored. The pump stays on the day target — and nothing in the house tells you, until it's cold.

So the trigger isn't "the night zone started", it's "something changed, or we restarted, or fifteen minutes passed". The action then works out what the current zone demands and sends it only if the heat pump isn't already there. That's idempotent, so running it after a restart is correct rather than something to suppress, and a missed boundary repairs itself at the next quarter-hour at the latest.

What keeps it safe is the conditions, not the trigger: the zone has to be a real value (unknown and unavailable fall out), the hub's clock has to be synced, and you have to have confirmed the tariff plan. If any of those fails, nothing is sent and the heat pump keeps running its own programme.

Nudge the setpoint; don't gate the supply. Holding the pump off until the cheap zone starts is the obvious idea and the wrong one: compressors need their own cycling, defrost runs and anti-legionella cycles.

The unit's own grid-signal input — SG Ready, in most of Europe — can block it, including a hard lockout of up to two hours, and utility lockout contacts are normal on heat-pump tariffs. The difference is that the manufacturer's controller still owns defrost and legionella scheduling while it's blocked, and knows when to override. A smart plug in the supply line owns none of that. So: the setpoint, a preset or a mode, through the unit's own interface — never its supply.

Make "no data" the safe state — and know what that leaves behind. If the hub goes offline the sensor turns unavailable, the conditions fail and nothing is sent, so the heat pump carries on under its own programme. That's the right behaviour for the appliance.

Be clear about what it isn't, though: if the day automation had already lowered your setpoint, nothing restores it while the hub is away. Silence protects the machine, not your comfort. That's precisely why the reconcile design matters — once the hub is back, the next tick puts the setpoint where the current zone says it should be, without you doing anything.

Run it as a notification first. For a day or two, replace the climate.set_temperature action with

yaml
- action: persistent_notification.create
        data:
          title: "Tariff zone"
          message: "Would set the heat pump to {{ target }} °C"

and watch when it would have fired. It costs nothing, and it's how you find out that your zone boundary isn't where you thought it was — or that your climate entity isn't the one you assumed.

If you have no climate entity in Home Assistant, skip this section entirely. The measurement and the cost-by-zone picture stand on their own, and an automation you can't drive properly is worse than none.

What this doesn't do

  • RBgrid never switches anything. It has no outputs. Every action in this guide is Home Assistant acting on your own equipment.
  • Per-circuit cost stays in the app. Home Assistant gets the heat pump's energy, not its cost.
  • No cost by zone per period. The zone split is a running total since the last hub restart.
  • The role isn't visible in HA, so an automation can't tell a heating point from any other.
  • A hub whose clock hasn't synchronized still reports a zone and still prices energy, from whatever time it thinks it is. That's why the automation above checks time_src, and why the first figures after a fresh boot deserve a second look.
  • RBgrid shows what the pump's energy cost at your rates. It doesn't check your supplier's invoice, and it makes no claim about what you'll save.

Next