Close

Revisiting state transitions

A project log for Laundry monitoring

A collection of notes and pointers for the usual laundry monitoring project.

wjcarpenterWJCarpenter 07/14/2023 at 03:130 Comments

This is another look at the states and event transitions that I described in an earlier project log. I'm now ready to implement it in a Home Assistant automation definition. It will actually be two automations, but the automation for the dryer is just a clone of the automation for the washer with the names of devices and entities adjusted.

As a reminder, there are 4 event inputs for each machine: door open, door closed, power on, and power off. Those are events, not states, so "power on" means "power is turned on". I'm configuring the automations as "queued", so the events can never happen simultaneously, no matter how it happens in real life. There are some impossible or nonsense transitions, and there are also some fairly simple considerations.

Current
state
Door
open
Door
closed
Power
on
Power
off
UnloadedLoading-RunningStopped
Loading-LoadedRunningStopped
LoadedLoading-RunningStopped
Running--RunningStopped
StoppedUnloading-RunningStopped
Unloading-UnloadedRunningStopped
Unknown--RunningStopped

This leads to fairly simple logic. Power events don't need to consider anything else. Door events only need to consider the current state to decide what the next state should be. There are some cases where things are not perfect, but we can tolerate them. For example, when in the STOPPED state and the door is opened, we transition to the unloading state. It will be very common for the machine to be re-loaded without the door being closed in the meantime. When the door is closed, we will think we are in the UNLOADED state when we are actually in the LOADED state. In the normal case, the power will soon turn on and transition us to the RUNNING state. We don't care that we missed the LOADING and LOADED states, except that we don't necessarily have the opportunity to alert if the machine is loaded but not powered on for some amount of time. That's not much different from someone opening the door, not loading the machine, and then closing the door. We would need some additional sensors of some kind to distinguish those cases, and it's not worth it (to me).

For the automation, I am using just one automation per machine, and the trigger can be any of the four event types. Even though it might seem simpler to have separate automations for each of the possible triggers, or at least for the possible trigger types, I'm combining them all into a single automation so that I can configure it as "queued" and not have to worry about concurrency edge cases.

I mentioned in an earlier project log that I am using templated sensors for the door contact sensors. That avoids having to deal with unavailable or unknown values. I'm using the templated forms for the automation triggers. Incidentally, I had to be careful to use entity state changes as the triggers. I originally used the contact sensor device, but that triggers on any attribute change. The devices phone home every few hours even if the door does not open or close. I think that's to report their battery level as an attribute.

For the power triggers, I used a numeric value comparison. Power above a threshold value for a certain period of time is considered on. Power below that threshold for a certain period of time is considered off. At the moment, I'm using 8 watts for the threshold and 1.5 minutes for the time period. I may have to tweak those things later, depending on what I observe in real life.

The action in the automation is just changing the state of the applicable dropdown helper entity. It consists of a nested choose action that implements the transition table. The default action is to do nothing, so we only have to deal with the table cells that have entries. The outer choose action is based on which trigger invoked the automation. The action for the power triggers is constant. For the door triggers, each has an inner choose action the distinguishes the current state.

(Update: I later added a 1 minute delay to the contact sensors to avoid some noise. If someone opens the dryer door to see if it's got something in it, it can move the state from STOPPED to UNLOADING and then UNLOADED. This can matter for the case of automatically cancelling a notification if we get fooled into thinking the dryer was unloaded when it wasn't.)

That all look pretty complicated in YAML. Here's a partial look at the GUI version to give a flavor of it.

And here is (most of) the actions for the door closed trigger:

Here's the complete YAML form:

alias: State transitions Washer
description: ""
trigger:
  - platform: numeric_state
    id: power_on
    entity_id: sensor.washer_power
    above: 8
    for:
      hours: 0
      minutes: 1
      seconds: 30
  - platform: numeric_state
    id: power_off
    entity_id: sensor.washer_power
    below: 8
    for:
      hours: 0
      minutes: 1
      seconds: 30
  - platform: state
    entity_id:
      - binary_sensor.contact_sensor_washer_triggered
    to: "on"
    id: door_open
  - platform: state
    entity_id:
      - binary_sensor.contact_sensor_washer_triggered
    to: "off"
    id: door_closed
condition: []
action:
  - choose:
      - conditions:
          - condition: trigger
            id: power_on
        sequence:
          - service: input_select.select_option
            data:
              option: Running
            target:
              entity_id: input_select.washer_state
      - conditions:
          - condition: trigger
            id: power_off
        sequence:
          - service: input_select.select_option
            data:
              option: Stopped
            target:
              entity_id: input_select.washer_state
      - conditions:
          - condition: trigger
            id: door_open
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: input_select.washer_state
                    state: Unloaded
                sequence:
                  - service: input_select.select_option
                    data:
                      option: Loading
                    target:
                      entity_id: input_select.washer_state
              - conditions:
                  - condition: state
                    entity_id: input_select.washer_state
                    state: Loaded
                sequence:
                  - service: input_select.select_option
                    data:
                      option: Loading
                    target:
                      entity_id: input_select.washer_state
              - conditions:
                  - condition: state
                    entity_id: input_select.washer_state
                    state: Stopped
                sequence:
                  - service: input_select.select_option
                    data:
                      option: Unloading
                    target:
                      entity_id: input_select.washer_state
      - conditions:
          - condition: trigger
            id: door_close
        sequence:
          - choose:
              - conditions:
                  - condition: state
                    entity_id: input_select.washer_state
                    state: Loading
                sequence:
                  - service: input_select.select_option
                    data:
                      option: Loaded
                    target:
                      entity_id: input_select.washer_state
              - conditions:
                  - condition: state
                    entity_id: input_select.washer_state
                    state: Unloading
                sequence:
                  - service: input_select.select_option
                    data:
                      option: Loaded
                    target:
                      entity_id: input_select.washer_state
mode: queued
max: 10

Discussions