It’s been a while since I investigated Home Assistant’s concurrency model. I was worried about contention between different automations manipulating the same resource. I came to the conclusion that all automations manipulating the same resource should be merged into one, using the composite automation pattern.

Now it’s time to put theory into practice. I have two automations that modify the charging settings of my Alpha ESS Home Battery. Once changes the settings to prevent the battery discharging while my EV is charging, the other sets a target SOC (state of charge) based on tomorrow’s solar generation and heating forecasts.

The problem is that there’s a single action which changes all the settings. Each automation does the equivalent of a read-modify-write to change the settings its interested in. If the two automations run concurrently you can end up in a mess. At the moment I try to avoid the problem by running the target SOC automation at a time when I’m unlikely to charge the EV and blocking it from running if the EV is charging.

Let’s see if we now have the tools to do this right.

Composite Automation Pattern

The Composite Automation Pattern uses multiple triggers combined with a choose action. The choose action executes a different set of sub-actions for each trigger id. In my case, I’ll have triggers for start charging, end charging and forecast updated.

The automation uses queued mode which prevents concurrent execution and makes sure that any concurrent triggers are queued up and run when the previous invocation has completed.

Refactoring

The Composite Automation Pattern works best when automations are focused on modifications of the shared resource. In queued mode, automation actions are equivalent to a critical section. You want to keep them as short as possible and in particular avoid including unrelated asynchronous operations.

Anything that doesn’t need to be inside the critical section should be factored out into a separate automation.

Extracting Dispatched Off-Peak Tracking

The current battery management automation is conceptually simple. We force home battery charging on when Octopus has scheduled a smart charge session. If this happens during peak hours we’ll only be charged off-peak rates. The difficult part is determining when these “dispatched” off-peak periods occur.

The Octopus API accessed via the Octopus Home Assistant integration reports planned dispatches. These are the periods when smart charging is planned. They change often, sometimes at the last minute before a session starts.

The planned dispatches data is retrieved via polling a rate limited API. It can be up to five minutes out of date. Sometimes the car starts charging before the dispatch is confirmed.

Instead of using the planned dispatches data, the automation uses EV charging as the trigger. Unfortunately, there’s more complexity. There are occasional transient spikes of charging, typically only a few seconds and low total energy used. I don’t know if this is a problem with Octopus or the charger. The spikes don’t trigger off-peak rates. There’s extra logic to filter them out.

The current automation is mixing concerns. The battery management automation should react to start/end dispatched off-peak periods. All the complexity of figuring out when those periods start and end should be factored out.

Dispatched Off-Peak Automation

I added a boolean helper to represent the “Dispatched Off-Peak” state, together with an automation that turns it on and off. This approach gives a lot more flexibility than trying to implement it all-in-one as a template sensor.

I’m using the composite automation pattern here too, mostly to keep all the logic in one place. There’s no concurrency issues as there are no asynchronous operations. We’re just reacting to changes in state and updating our helper state.

Extracting the logic like this made it easy to fix a couple of nagging issues. First, the hypervolt_charging state sometimes takes a minute or two to change to off when a dispatched period ends. When this happens the charging current drops to 0.7A immediately and then drops to 0A after a minute or two. I added hypervolt_charger_current < 1A as an additional trigger for the end of charging.

That introduces a new transient spike. Sometimes the charger current will drop to 0.7A and then jump back up to 30A after a minute or two. This typically happens when there are two back-to-back dispatched periods. To handle this I added hypervolt_charger_current > 2 as an additional trigger for the start of charging.

Finally, I now make the most of the Octopus pricing structure. Prices are fixed during each half-hour period during the day. If a dispatch only covers part of a half-hour period, you are still charged off-peak rates for the entire period. If the charger is turned off part way through a half-hour period, I leave dispatched_off_peak_electricty on until the end of the current half-hour.

As time isn’t precisely synchronized between Home Assistant, the Hypervolt Charger and the Octopus backend systems, I treat any end of charging within two minutes of a half-hour boundary as the end of a period.

triggers:
  - trigger: numeric_state
    entity_id: sensor.hypervolt_session_energy
    above: 10
    id: hypervolt_on
  - trigger: numeric_state
    entity_id: sensor.hypervolt_charger_current
    above: 2
    id: hypervolt_on
  - trigger: time_pattern
    minutes: /30
    id: end_period
  - trigger: state
    entity_id: switch.hypervolt_charging
    from: "on"
    to: "off"
    id: hypervolt_off
  - trigger: numeric_state
    entity_id: sensor.hypervolt_charger_current
    below: 1
    id: hypervolt_off
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: hypervolt_on
          - condition: state
            entity_id: switch.octopus_energy_XXXXX_intelligent_smart_charge
            state: "on"
          - condition: state
            entity_id: switch.hypervolt_charging
            state: "on"
          - condition: numeric_state
            entity_id: sensor.hypervolt_session_energy
            above: 10
        sequence:
          - action: input_boolean.turn_on
            metadata: {}
            target:
              entity_id: input_boolean.dispatched_off_peak_electricity
            data: {}
      - conditions:
          - condition: trigger
            id: end_period
          - condition: state
            entity_id: input_boolean.dispatched_off_peak_electricity
            state: "on"
          - condition: state
            entity_id: switch.hypervolt_charging
            state: [ "off", unknown, unavailable ]
        sequence:
          - action: input_boolean.turn_off
            metadata: {}
            target:
              entity_id: input_boolean.dispatched_off_peak_electricity
            data: {}
      - conditions:
          - condition: trigger
            id: hypervolt_off
          - condition: template
            alias: Within a minute of 30 minute period boundary
            value_template: |-
              {% set minute = now().minute %}
              {{ minute > 58 or minute < 2 or (28 < minute < 32) }}
        sequence:
          - action: input_boolean.turn_off
            metadata: {}
            target:
              entity_id: input_boolean.dispatched_off_peak_electricity
            data: {}
mode: queued
max: 4

Extracting “Dispatched Off-Peak Electricity” as a separate helper lets us reuse the logic for other purposes. I have a template sensor that tracks overall “Off-Peak” periods combining scheduled and dispatched. Previously, that duplicated this logic. Now, it’s a simple template sensor that combines two other entity values.

Home Battery Management Automation

After that refactoring, the battery management automation is far simpler, and just focused on changing the battery settings.

alias: Force charge battery during Octopus Intelligent Smart Charge
triggers:
  - trigger: state
    entity_id: input_boolean.dispatched_off_peak_electricity
    to: "on"
actions:
  - action: alphaess.setbatterycharge
    data:
      cp2start: "05:30"
      cp2end: "23:30"
      chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
  - wait_for_trigger:
      - trigger: state
        entity_id: input_boolean.dispatched_off_peak_electricity
        to: "off"
  - action: alphaess.setbatterycharge
    data:
      cp2start: "00:00"
      cp2end: "00:00"
      chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
mode: restart

Cold Weather Day Time Car Charging

The changes so far are already a win. I have a 10kWh home battery. It’s not big enough to run the heat pump all day during cold weather. Once the battery runs out, we have to use peak price electricity directly from the grid.

Octopus are changing their terms for the Intelligent Go tariff. Smart charging at off-peak prices will be limited to 6 hours a day. The idea is to encourage you to plug the car in more often, giving Octopus more opportunity to optimize when charging happens.

I’m happy to comply. I used to add 50% once or twice a week. I now plug the car in first thing each morning, adding 10% each day. This gives Octopus the maximum chance of scheduling day time charging periods.

Day Time Charging Scheduled by Octopus
×
Day Time Charging Scheduled by Octopus

The Octopus Home Assistant integration provides an “Intelligent Dispatching” entity which uses planned dispatches responses from the API to predict when smart charging is occurring. Notice that for the second period it starts 5 minutes after the car starts charging. This is because the previous poll of the API happened just before the start of the dispatch.

Our “Dispatched Off Peak Electricity” helper is bang on, mirroring the Hypervolt charger. Octopus helpfully scheduled an extra 5 minutes of charging after 13:00 which means the entire 13:00 to 13:30 period should be charged at off-peak rates. We take full advantage, extending the “Dispatched Off Peak” period to the next half hour boundary.

Day Time Charging Octopus Charges
×
Day Time Charging Octopus Charges

Yes, I did confirm that Octopus charged off-peak rates for the entire 12:00 to 13:30 period. That gave us enough extra juice for the battery to last until peak shower time in the evening.

Day Time Hot Water Boost

We heat hot water overnight during the scheduled off-peak period and then on-demand during the day if we run out. Now that I have a dispatched_off_peak_electricity entity, I can schedule additional pre-emptive hot water runs during dispatched off peak periods during the day.

Some care is needed. A hot water run takes 20-30 minutes and unlike EV charging can’t be stopped and started without wasting a lot of energy. I trigger a hot water boost in the first few minutes of a half-hour period so I know that the off-peak rate will last for long enough.

triggers:
  - trigger: state
    entity_id: input_boolean.dispatched_off_peak_electricity
    to: "on"
    for: { hours: 0, minutes: 2, seconds: 30 }
    id: off_peak_start
  - trigger: time_pattern
    minutes: "02"
    id: period_start
  - trigger: time_pattern
    minutes: "32"
    id: period_start
conditions:
  - condition: time
    after: "09:00:00"
    before: "21:00:00"
  - condition: numeric_state
    entity_id: sensor.home_domestic_hot_water_0_tank_temperature
    below: 45
  - condition: state
    entity_id: switch.home_domestic_hot_water_0_boost
    state: "off"
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: off_peak_start
          - alias: Started within first 10 minutes of period
            condition: template
            value_template: |-
              {% set minute = now().minute %}
              {{ (1 < minute < 10) or (31 < minute < 40) }}
        sequence:
          - action: switch.turn_on
            target:
              entity_id: switch.home_domestic_hot_water_0_boost
      - conditions:
          - condition: trigger
            id: period_start
          - condition: state
            entity_id: input_boolean.dispatched_off_peak_electricity
            state: "on"
            for: { hours: 0, minutes: 2, seconds: 0 }
        sequence:
          - action: switch.turn_on
            target:
              entity_id: switch.home_domestic_hot_water_0_boost
mode: single

There’s two different cases to handle. The first case is when the dispatch starts during the first 10 minutes of the half-hour. The second is when the dispatch started later in the previous half-hour but is still active at the start of the next half-hour.

Home Battery Management Refactor

All that’s left is to complete the Home Battery Management refactor and switch to the composite automation pattern.

I’ve kept the existing separate automation that calculates a target SOC based on solar and heating forecasts. I’ve removed the final action that changes the SOC setting on the battery. That’s now done by the battery management automation, triggered by the change in alpha_ess_target_soc.

  - trigger: state
    entity_id: input_boolean.dispatched_off_peak_electricity
    to: "on"
    id: off_peak_on
  - trigger: state
    entity_id: input_boolean.dispatched_off_peak_electricity
    to: "off"
    id: off_peak_off
  - trigger: state
    entity_id: input_number.alpha_ess_target_soc
    to: null
    id: update_soc
actions:
  - choose:
      - conditions:
          - condition: trigger
            id: off_peak_on
        sequence:
          - action: alphaess.setbatterycharge
            data:
              cp2start: "05:30"
              cp2end: "23:30"
              chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
      - conditions:
          - condition: trigger
            id: off_peak_off
        sequence:
          - action: alphaess.setbatterycharge
            data:
              enabled: true
              cp2start: "00:00"
              cp2end: "00:00"
              chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
      - conditions:
          - condition: trigger
            id: update_soc
        sequence:
          - if:
              - condition: state
                entity_id: input_boolean.dispatched_off_peak_electricity
                state: "on"
            then:
              - action: alphaess.setbatterycharge
                data:
                  cp2start: "05:30"
                  cp2end: "23:30"
                  chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
            else:
              - action: alphaess.setbatterycharge
                data:
                  cp2start: "00:00"
                  cp2end: "00:00"
                  chargestopsoc: "{{ states('input_number.alpha_ess_target_soc') }}"
mode: queued
max: 4

When updating the battery SOC, I need to make sure the charging period is set as it should be based on the “Dispatched Off-Peak” helper. I could have used some templating magic to do it with a single action but it was quicker to add an if and copy the setbatterycharge actions I already had for the then and else clauses.

Conclusion

I now have a good foundation for more complex logic. For example, in the summer I could let the battery discharge to target SOC if it was above the desired level when charging starts. I could re-calculate the target SOC several times during the day based on current state and the forecast for the remains of the day.