AppDaemon state machines

Hi,

I found myself implementing state machines in AppDaemon again and again, and sometimes even in Home Assistant’s yaml configuration. This lead me to writing and publishing a general-purpose state machine library. For me, it makes it much easier to understand state transitions.

Have a look and let me know if it’s useful for you too.

— Przemek

3 Likes

maybe you could do a litlle explaining also.

i have no clue what state machines is, and what to use it for and how.
if you explain why you use it and where you use it for, maybe other get interested as well.

Very cool!

@ReneTode: state machines are a programming concept: https://en.wikipedia.org/wiki/Finite-state_machine

Home Assistant itself uses a state machine to track the states of its entities: https://developers.home-assistant.io/docs/en/architecture_index.html

@PeWu’s app provides a declarative syntax to define state transitions and, upon “seeing” them occur, perform actions. If you look at the example app provided in his GitHub repo:

import appdaemon.plugins.hass.hassapi as hass
from enum import Enum
from machine import Machine, ANY, IsState, IsNotState, Timeout

class States(Enum):
  HOME = 1
  AWAY = 2
  LEAVING = 3
globals().update(States.__members__) # Make the states accessible without the States. prefix.

class Presence(hass.Hass):
  def initialize(self):
    machine = Machine(self, States)

    machine.add_transitions(ANY, IsState('device_tracker.my_phone', 'home'), HOME)
    machine.add_transition(HOME, IsNotState('device_tracker.my_phone', 'home'), LEAVING)
    machine.add_transition(LEAVING, Timeout(30), AWAY, on_transition = self.on_away)

    machine.log_graph_link()

  def on_away(self):
    # e.g. turn off the lights.

…you’ll see a declarative syntax for three state transitions:

  1. When device_tracker.my_phone has a state of home in HASS and previously had any state in this app, set its state in this app to be HOME.
  2. When device_tracker.my_phone has any state besides home in HASS and previously had a HOME state in this app, set its state in this app to be LEAVING.
  3. When device_tracker.my_phone has had a LEAVING state in this app for 30 seconds, set its state in this app to be AWAY and fire the on_away method.

It’s definitely a rather complicated concept, but it saves the implementer from repetitively using listen_state to define similar state behaviors for multiple entities.

@ReneTode: Thanks for the comment. I should have given some more details.

@bachya: Thank you for helping me out with the explanations!

Here is a visualization of the state machine from the example:
machine

so its actually just a translation from:

def init(...):
  self.listen_state(self.cb,"device_tracker.my_phone")
def cb(...):
  if self.timer:
    self.cancel_timer(self.timer)
  if new=="HOME and old!="HOME":
    self.state = "HOME"
  elif old == "HOME" and new!="HOME":
    self.state = "LEAVING"
    self.timer = self.run_in(self.on_way,30)

Yes, mostly. The devil is in the detail.
I believe it is harder to make a mistake when defining a state machine. Once you start watching more than one entity, the if-else code becomes more difficult to maintain and the edge cases are harder to spot.

1 Like

i think it also depends on what you are used to.
if it really compares the state from the app with that from HA it wouldnt be anything for me, because i dont keep states in my apps. (except for maybe a rare occasion)

my base is HA, and only the HA state is important to me.

I have to admit that, although it looks very clever, I am having a hard time thinking of an app I would use it for.

What sort of apps are you writing that need there own state machine?

3 Likes

As @gpbenton said, I am not certain where I will need this. Looking at the code @ReneTode presented, I understand what is said when more entities are used, but I can easily get around that by either reusing the module or just storing the states in dictionaries.

Even if I don’t want to store it in dictionaries, I pass a lot of kwargs when running functions, and I use that to track who has what.

So hard time thinking where it will be used, though looks :sunglasses:

1 Like

Very cool! (entering code review mode - feel free to ignore all of these)

  • It might be nice to have access to the from and to states in the callbacks. I don’t have a specific example in mind but it seems like that could be useful at some point.
  • Another improvement might be to allow an arbitrary callback to be supplied to the transition w/ additional (optional) args. This would allow using creating callbacks that can do more complicated things and you could pass them in using something like functools.partial() to specify keywords to be used with the callback for specific transitions.
  • It could be just my brain, but IsState and IsNotState don’t say “equal” and “not equal” to me. I read those as “is this a state”. At least for the way I think, “IsStateEq” and “IsStateNeq” would be clearer.
  • I was trying to figure out what kinds of new triggers could be written so I went looking for a Trigger API in the code. But it appears the triggers are a list of isinstance() statements in the code. That will make it difficult to expand the functionality. It would be nice if a trigger was a class with defined methods - then people could write their own trigger classes and pass them in.

All in all it looks like a great addition to the appdaemon toolbox!

with listen_state you got that. they are called old and new :wink:

1 Like

Would this allow HADashboard to update whenever my HA devices update?

your dashboard will always update when HA updates, unless you got a problem somewhere and the dashboard isnt working correct or you use the wrong browser (settings)

Thanks I guess I have a problem with my settings, or the way I have HADashboard setup, as I had to keep manually refreshing the page in order for some of my devices to update, I’ll have to give it another go :slight_smile:

please feel free to open another topic if you still got that problem, because manually refresh shouldnt be neccesary.

Thanks @TD22057 for the comments.
To address your comments…

  1. The callback passed to Machine.on_transition() receives from_state and to_state as arguments. The on_transition argument of add_transition() takes a 0-argument callback because the from- and to- states are known at call time. That leaves us with add_transitions() that adds multiple transitions and this is a good candidate for changing from taking a 0-arg callback to 2-arg.
  2. I don’t understand this idea. You can already do this:
...
  add_transition(STATE1, Timeout(5), STATE2, on_transition = functools.partial(self.cb, 12))
...
def cb(self, x):
  # x = 12
  1. Naming is not my strong point :slight_smile: . I took the name from is_state() in https://www.home-assistant.io/docs/configuration/templating/
    I also handle binary sensors with the same trigger by writing IsState('binary_sensor.switch1') and IsNotState('binary_sensor.switch1'). Maybe it’s better not to mix them and have: StateEq, StateNeq, StateOn, StateOff. The last 2 for binary sensors.
  2. An extendable trigger API would be nice but it would take some work to do it. I’m not sure there are really that many other useful triggers. It is always possible to implement it at one point.

Here is an example based on the config by @jimpower for monitoring the washing machine.
Original config: https://github.com/JamesMcCarthy79/Home-Assistant-Config/blob/master/config/packages/appliances/appliances.yaml#L218-L309

State machine definition in AppDaemon:

  def initialize(self):
    machine = Machine(
        self, States, initial = IDLE,
        entity = 'input_select.washing_machine_status')

    machine.add_transitions(
        [IDLE, CLEAN, FINISHING],
        StateOn('binary_sensor.washing_machine_active'),
        RUNNING)
    machine.add_transition(
        RUNNING, StateOn('binary_sensor.washing_machine_inactive'), FINISHING)
    machine.add_transition(FINISHING, Timeout(60), CLEAN)
    machine.add_transition(
        [CLEAN, FINISHING],
        StateOn('binary_sensor.door_window_sensor_158d0001e73a83'),
        IDLE)

The state machine triggers don’t support expressing attributes.load_power > 10, so some yaml is needed:

binary_sensor:
- platform: template
  sensors:
    washing_machine_active:
      value_template: >-
        {{ states.switch.plug_158d0001bc2b6d.attributes.load_power > 10 }}
    washing_machine_inactive:
      value_template: >-
        {{ states.switch.plug_158d0001bc2b6d.attributes.load_power < 6 }}

With some more work, the state machine library can be extended to natively support arbitrary conditions on entities and attributes.

Having this code in Python in AppDaemon now makes it possible to reuse the same state machine to run for the dryer (in the same config by @jimpower) by adding parameters.

do i understand correct that this creates entities in appdaemon that are not in homeassistant?

In the above example, the input_select.washing_machine_status entity is updated in Home Assistant when the state of the state machine changes. If you define the input_select explicitly in Home Assistant, you get a dropdown in the UI and it will be updated from AppDaemon. The state machine doesn’t update itself if you change the state in the UI – this is a feature that I’d like to add.

If you don’t define this entity in Home Assistant, AppDaemon will create this entity but the UI dropdown will not show up.

hmm, so at this moment you just got variables in appdaemon that live their own live without impacting HA.
unless you create the same entity in HA first then this entity updates HA but not the other way around.

and you create even template entities that only live in AD?

for this to be really usefull you need to make sure that every change in AD does also change the entitystate in HA and the other way around, and that you dont create entities in AD that only live there.
or else you will very fast lose track of what entities exist and what doesnt, and you will have a hard time knowing what state entities have.