Complex Automations - State Machine

Entities

Here’s a summary of all the entities required for this 5-zone controller:

  • One timer to countdown a zone’s duration.
  • One counter to keep track of which zone to process.
  • One input_select to control overall operation (on/off/pause).
  • One group containing all the zone switches.
  • Five input_booleans, one per zone to indicate its enabled/disabled status.
  • Five input_datetimes, one per zone to indicate its duration (in hours and minutes).
  • Five more input_booleans, one per zone representing each zone switch (controlling a valve in an irrigation system).

Note that the last set of 5 input_booleans should be 5 switches. However, for demonstration purposes, it’s easier to use input_booleans. After you have finished experimenting with this example, you can modify its code to control actual switches as opposed to input_booleans.

Here’s the configuration for all of the entities.

Summary
counter:
  zone:
    initial: 0
    step: 1

input_select:
  controller_mode:
    name: Zone Controller Mode
    options:
      - 'off'
      - 'on'
      - 'pause'

input_boolean:
  zone_1:
    name: Zone 1
  zone_2:
    name: Zone 2
  zone_3:
    name: Zone 3
  zone_4:
    name: Zone 4
  zone_5:
    name: Zone 5
  switch_zone_1:
    name: "Switch Zone 1"
  switch_zone_2:
    name: "Switch Zone 2"
  switch_zone_3:
    name: "Switch Zone 3"
  switch_zone_4:
    name: "Switch Zone 4"
  switch_zone_5:
    name: "Switch Zone 5"

input_datetime:
  zone_1:
    name: Zone 1
    has_date: false
    has_time: true
  zone_2:
    name: Zone 2
    has_date: false
    has_time: true
  zone_3:
    name: Zone 3
    has_date: false
    has_time: true
  zone_4:
    name: Zone 4
    has_date: false
    has_time: true
  zone_5:
    name: Zone 5
    has_date: false
    has_time: true

group:
  zones:
    name: Zones All
    entities:
      - input_boolean.switch_zone_1
      - input_boolean.switch_zone_2
      - input_boolean.switch_zone_3
      - input_boolean.switch_zone_4
      - input_boolean.switch_zone_5

Automation

The single automation contains about 130 lines of YAML. It uses three triggers:

  1. State Trigger for the input_select which governs the controller’s operation (on/off/pause).
  2. State Trigger for the counter which is used to step through the five zones.
  3. Event Trigger for the timer which is used to control a zone’s operation.

The automation’s mode is queued because it performs actions that will cause it to be triggered (i.e. it calls itself).

Summary
- alias: Zone Controller
  id: zone_controller
  mode: queued
  variables:
    zone_max: 5
  trigger:
  - id: 'controller_mode'
    platform: state
    entity_id: input_select.controller_mode
  - id: 'zone_index'
    platform: state
    entity_id: counter.zone
  - id: 'zone_timer'
    platform: event
    event_type:
    - timer.started
    - timer.finished
    - timer.paused
    - timer.restarted
    - timer.cancelled
    event_data:
      entity_id: timer.zone
  action:
  - variables:
      z_index: "{{ states('counter.zone') | int }}"
      z_switch: "input_boolean.switch_zone_{{ z_index }}"
      z_mode: "input_boolean.zone_{{ z_index }}"
      z_duration: "input_datetime.zone_{{ z_index }}"

  - choose:
    - conditions: "{{ trigger.id == 'controller_mode' }}"
      sequence:
      - choose:
        - conditions: "{{ trigger.to_state.state == 'on' }}"
          sequence:
          - choose:
            - conditions: "{{ z_index == 0 }}"
              sequence:
              - service: counter.increment
                target:
                  entity_id: counter.zone
            - conditions: "{{ z_index <= zone_max and trigger.from_state.state == 'pause' }}"
              sequence:
              - service: timer.start
                target:
                  entity_id: timer.zone
            default:
            - service: counter.reset
              target:
                entity_id: counter.zone
        - conditions: "{{ trigger.to_state.state == 'off' }}"
          sequence:
          - service: homeassistant.turn_off
            target:
              entity_id: group.zones
          - service: timer.cancel
            target:
              entity_id: timer.zone
        - conditions: "{{ trigger.to_state.state == 'pause' and trigger.from_state.state != 'off' }}"
          sequence:
          - service: timer.pause
            target:
              entity_id: timer.zone
        default:
        - service: input_select.select_option
          target:
            entity_id: input_select.controller_mode
          data:
            option: 'off'

    - conditions: "{{ trigger.id == 'zone_index' }}"
      sequence:
      - choose:
        - conditions: "{{ z_index == 0 }}"
          sequence:
          - service: input_select.select_option
            target:
              entity_id: input_select.controller_mode
            data:
              option: 'off'
        - conditions: "{{ z_index <= zone_max }}"
          sequence:
          - choose:
            - conditions: "{{ is_state(z_mode, 'on') }}"
              sequence:
              - service: timer.start
                target:
                  entity_id: timer.zone
                data:
                  duration: "{{ state_attr(z_duration, 'timestamp') }}"
            default:
            - service: counter.increment
              target:
                entity_id: counter.zone
        default:
        - service: counter.reset
          target:
            entity_id: counter.zone

    - conditions:
      - "{{ trigger.id == 'zone_timer' }}"
      - "{{ z_index != 0 }}"
      sequence:
      - choose:
        - conditions: "{{ trigger.event.event_type in ['timer.started', 'timer.restarted'] }}"
          sequence:
          - service: input_boolean.turn_on
            target:
              entity_id: "{{ z_switch }}"
        - conditions: "{{ trigger.event.event_type == 'timer.paused' }}"
          sequence:
          - service: input_boolean.turn_off
            target:
              entity_id: "{{ z_switch }}"
        - conditions: "{{ trigger.event.event_type == 'timer.cancelled' }}"
          sequence:
          - service: input_boolean.turn_off
            target:
              entity_id: "{{ z_switch }}"
          - service: counter.reset
            target:
              entity_id: counter.zone
        - conditions: "{{ trigger.event.event_type == 'timer.finished' }}"
          sequence:
          - service: input_boolean.turn_off
            target:
              entity_id: "{{ z_switch }}"
          - service: counter.increment
            target:
              entity_id: counter.zone

Lovelace UI

I have prepared four Entities Cards in order to display all the zones and control their operation. They are all within a view called Zone Controller (so paste the YAML code under views: when using Lovelace’s Raw Configuration Editor … or build the cards from scratch).

Summary
  - title: Zone Controller
    path: zone-controller
    badges: []
    cards:
      - type: entities
        entities:
          - entity: input_boolean.zone_1
          - entity: input_boolean.zone_2
          - entity: input_boolean.zone_3
          - entity: input_boolean.zone_4
          - entity: input_boolean.zone_5
        title: Zone Mode
        show_header_toggle: false
      - type: entities
        entities:
          - entity: input_datetime.zone_1
          - entity: input_datetime.zone_2
          - entity: input_datetime.zone_3
          - entity: input_datetime.zone_4
          - entity: input_datetime.zone_5
        title: Zone Duration
      - type: entities
        entities:
          - entity: input_boolean.switch_zone_1
          - entity: input_boolean.switch_zone_2
          - entity: input_boolean.switch_zone_3
          - entity: input_boolean.switch_zone_4
          - entity: input_boolean.switch_zone_5
        title: Zone Switch
        show_header_toggle: false
      - type: entities
        entities:
          - entity: input_select.controller_mode
          - entity: counter.zone
          - entity: timer.zone
        title: Controller

Here’s a screenshot of the UI with the controller in operation. Only zones 1, 2, and 5 are enabled. Each one is set to run for just 1 minute. It has already completed zone 1 and is currently working on zone 2 (you can see Switch Zone 2 is on and the zone counter indicates 2). The timer shows there are 35 seconds remaining before zone 2 is turned off and it moves on to activate zone 5.

Operation

In the Zone Modes card, set a few zones to on. The values you set will survive a restart (i.e. they will not reset to off).

In the Zone Duration card, set the input_datetimes to whatever values you want. The values you set will survive a restart (i.e. they will not reset to 00:00).

The Zone Switch card requires no configuration. It’s there just to show you which “Zone Switch” is currently in operation.

In the Controller card, set Zone Controller Mode to on. It should immediately activate the Zone Switch corresponding to the first enabled Zone. The zone timer will display the remaining time.

While it’s operating, you can set the input_select to pause and the currently activated zone will be deactivated and the timer paused. Set it back to on and the zone will be re-activated and the timer will continue from where it left off. If you set it to off it not only deactivates the current zone but, for good measure, turns off all zones (that’s what the group is used for). If you allow it to complete all enabled zones, it will automatically set the input_select to off.

If the controller’s mode is off and you set it to pause, it automatically sets itself back to off (because off → pause is a meaningless command).


NOTE

Rather than explain the automation’s operation step by step (a great deal of work), I would prefer to answer specific questions about it.


EDIT

Correction. Replaced non-existent service call “group.turn_off” with “homeassistant.turn_off”.

7 Likes