Complex Automations - State Machine

What I am trying to do is not rocket science but it gets very complex very quickly.

I want to implement a state machine (or similar) for a couple of automations and I am struggling to find the right way as I think I am overcomplicating stuff.

I have managed to implement the state machine below by using a separate automation for each state transition.
graphviz (1)

The code for the four automation that runs the state machine above is (I am aware that IDLE and OFF are the same state but I want to keep them separated for clarity):

- id: '1630664468279'
  alias: Pool cycle [IDLE -> ON]
  description: ''
  trigger:
  - platform: state
    entity_id: input_boolean.trigger_timmer_pool_cycle
    to: 'on'
  condition:
  - condition: not
    conditions:
    - condition: state
      state: active
      entity_id: timer.cycle_pool_pause
  action:
  - service: timer.start
    data:
      duration: '{{ states(''input_number.pool_cycle_time'')|int|multiply(60)|timestamp_custom(''%H:%M'',
        false) }}'
    target:
      entity_id: timer.pool_cycle
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.trigger_timmer_pool_cycle
  - service: switch.turn_on
    target:
      entity_id: switch.100121d807
  - service: timer.cancel
    target:
      entity_id: timer.cycle_pool_pause
  mode: single
##################################################
- id: '1630664189228'
  alias: 'Pool cycle [ON -> OFF] '
  description: ''
  trigger:
  - platform: state
    entity_id: timer.pool_cycle
    to: idle
    from: active
  condition: []
  action:
  - service: switch.turn_off
    target:
      entity_id: switch.100121d807
  - service: timer.start
    data:
      duration: '{{         states(''input_number.pool_cycle_time'')|int|multiply(60)|timestamp_custom(''%H:%M'',         false)
        }}'
    target:
      entity_id: timer.cycle_pool_pause
  mode: single
##################################################
- id: '1630667123542'
  alias: 'Pool cycle [ON ->Pause] '
  description: ''
  trigger:
  - platform: state
    entity_id: timer.pool_cycle
    to: paused
  condition: []
  action:
  - service: switch.turn_off
    target:
      entity_id: switch.100121d807
  mode: single
##################################################
- id: '1630663903655'
  alias: Pool cycle [PAUSE-> ON]
  description: ''
  trigger:
  - platform: state
    entity_id: timer.pool_cycle
    to: active
    from: paused
  condition: []
  action:
  - service: switch.turn_on
    target:
      entity_id: switch.100121d807
  mode: single

The problem happens when more complex automations are required as the following aimed to the sprinkler system → Image here. But a whopping number of automation are required, around 18.

So, is there an easier way to bring this state machine to Home Assistant.

P.D: I have tried to use this library but Events are not implemented still and to be honest I am struggling to make it work with timers without a major rewrite.

If you know Python there is this:

Can you confirm that I have understand what you want? Correct me wherever I have deviated from your requirements.

Assume there are 5 sprinkler zones.

  • When the system is enabled, it begins stepping through each one of the 5 zones.
  • If a zone’s associated input_boolean is enabled, its corresponding switch is turned on for the duration specified by the zone’s associated input_number. A timer performs the countdown.
  • If the zone is disabled, the system simply skips to the next zone.
  • When the timer finishes, the zone’s switch is turned off, and the system proceeds to process the next zone.
  • After all enabled zones are processed, the system is set to idle.
  • At any time during its processing, the system may be paused. The current zone’s switch is turned off and the timer is paused. Re-enabling the system causes it to pick up from where it paused (i.e. switch is turned back on and timer continues to countdown the balance of its duration).
  • At any time during its processing, the system may be disabled. This causes it to cancel the timer and turn off all switches.

Wow! That is actually spot on!

A timeout would be handy in case that no more solar energy is available that day and the system stays on pause for too long. (That is the condition that it is making the flow stop) but that is a minor point.

What I am really looking for is a systematic way to do complex automation following a visual control flow so debugging becomes much easier. After developing the sprinkler system a clever energy management system is required to maximize solar power so what I really need to find is the right process to create these kinds of automations.

I have a single automation that achieves what I described (and supports any number of sprinkler zones). Adding a timeout for the pause state wouldn’t be a problem. Let me know if that interests you and I’ll post the details.

Hi Taras! Please feel free to post it! That would help me greatly to understand the way to do automations.

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

You can also use Node-RED with a state machine add-on like node-red-contrib-finite-statemachine (node) - Node-RED

Finally, I managed to create the automation in pyscript and I am quite happy with the result. Basically is the same automation presented by @123 but written in plain python and some minor tweaks.

The complete final solution can be found in this Github repo

Here is what the final interface and behaviour look like:

Thank you to everyone, especially to you @123 for the full automation with the thorough explanation and @tom_l for introducing me to pyscript.

2 Likes