8000 Else clause for automations · Issue #378 · home-assistant/architecture · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Else clause for automations #378

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
OnFreund opened this issue May 3, 2020 · 65 comments · Fixed by home-assistant/core#37818
Closed

Else clause for automations #378

OnFreund opened this issue May 3, 2020 · 65 comments · Fixed by home-assistant/core#37818

Comments

@OnFreund
Copy link
OnFreund commented May 3, 2020

Context

There are a lot of use cases that require one sequence of actions if a condition is true and different one if it's false. For example:

  • Checking if a person is authorized to perform an action, and then either performing it or notifying someone of an attempt.
  • Performing a different action based on whether it's a weekday or a weekend, home or away, etc...

The existing methods of performing this have their limitations. The easiest way is creating two automations with the same trigger and opposite conditions (this is now easier thanks to home-assistant/core#34624). This means repeating the conditions, which is both not DRY and, more importantly, could also in some cases lead to both automations running (or not running) since the conditions are evaluated twice.
One can avoid repeating the conditions by use of templates and input_*, but that could be cumbersome, requires redundant entities, and is still subject to potential timing issues.
Other approaches could include a service template to run two different scripts, but that means the conditions now need to run as actions instead of conditions. I'm sure there are other approaches as well, each with its limitation.

Keeping in mind that we don't want to turn automations into a general purpose programming language, I wanted to propose a relatively simple mechanism, that is able to achieve quite a lot.

Proposal

I propose adding an else clause to automation, which will be defined in a similar way to the existing actions, but will run when the condition is false, rather than when it's true.
This covers simple use cases easily, and more advanced cases can be handled through firing events and triggering multiple automations.

For example:

trigger:
  ...
condition:
  - condition: state
    entity_id: binary_sensor.weekday
    state: 'on'
action:
  <actions to perform on a weekday>
else_action:
  <actions to perform during the weekend>

Consequences

It's easier and less error prone to create automations that have if/else logic. Since the else clause is optional, existing automations aren't affected.

@balloob
Copy link
Member
balloob commented May 3, 2020

I think step 1 should be to add else logic to scripts, since scripts and the action part of automations are executed using the same code. Once that is in place, we might think differently about automations :)

A discussion is currently ongoing here home-assistant/core#35133 but I think that should be moved to arch.

I am 👍 on adding else.

@OnFreund
Copy link
Author
OnFreund commented May 3, 2020

OK, adding this to scripts enables a lot of use cases, perhaps even more than my proposal. I wanted to start small and intuitive - a single else clause per automation, but if we're ok taking a bolder approach I'm all for it.

@thomasloven
Copy link

If you don't mind, I think continuing the discussion here would make sense.

The PR linked above adds an else option to the condition step of script/automation.
I'm not convinced that's the best way since it inverts the flow from what you normally expect.

- condition: ...
  else:
    - condition failed
- condition passed

rather than

if
  then
else

The pros of this method is that it simply replaces the default behavior of a failed condition - to stop execution.
Perhaps this problem can be "fixed" by renaming the else parameter to on_fail or something...

@pvizeli
Copy link
Member
pvizeli commented May 4, 2020

The perfect solution I think would be to have multiple sequence:

script:
  example_script:
    sequence:
      - condition: state
         entity_id: device_tracker.paulus
         state: 'home'
      - service: notify.notify
        data:
          message: 'Turned on the ceiling light!'
    sequence:
      - condition: state
         entity_id: device_tracker.paulus
         state: 'away'
      - service: notify.notify
        data:
          message: 'Turned on the ceiling light!'

EDIT:
But currently that work just for script and not automation...

@OnFreund
Copy link
Author
OnFreund commented May 4, 2020
8000

Yes, I agree that on_fail or something similar would be more representative than else in that case. Overall, I think these are both legitimate ways of achieving something similar. I think a single else clause for an automation is a bit more intuitive, especially for users without programming background, but an on_fail approach can achieve more use cases without resorting to multiple automations.

@pvizeli
Copy link
Member
pvizeli commented May 4, 2020

Yeah, I like the idea from @thomasloven, it allow not too complex deeps as my variant, but it work out of the box everywhere and is fine for yaml.

@OnFreund
Copy link
Author
OnFreund commented May 4, 2020

@pvizeli my main concern with the multiple sequences approach is that the conditions are evaluated multiple times, which can potentially lead to confusing results. In your example, the device tracker can change from home to away between the sequences (or vice versa) and both services will be called (or neither). Unless the entire state machine is frozen during script execution..

@OnFreund
Copy link
Author
OnFreund commented May 4, 2020

To rephrase - I think there needs to be an atomic way to evaluate a condition and then branch out accordingly.

@balloob
Copy link
Member
balloob commented May 4, 2020

Maybe a new type branch with a list of condition + sequence. The first entry for which the condition passes will be executed.

@balloob
Copy link
Member
balloob commented May 4, 2020

The nice thing about a branch type is that you also can add generic code to be executed after either branch is done.

And we keep the condition code clean.

@thomasloven
Copy link
thomasloven commented May 4, 2020

I like it. That's a very good point with the common continuation after any branch too.

- branch:
  - condition: state
    entity_id: input_binary.guest_mode
    state: 'on'
    sequence:
      - service: system_log.write
        data:
          message: There are guest
  - condition: state
    entity_id: input_binary.guest_mode
    state: 'off'
    sequence:
      - service: system_log.write
        data:
          message: Social distancing
  - sequence:
    # No condition -> Always evaluates to true, i.e. an else-clause
      - service: system_log.write
        data:
          message: Something went horribly wrong!

@balloob
Copy link
Member
balloob commented May 5, 2020

I would prefer if we would not extend condition schemas but instead have condition and sequence live in a dictionary. This will make validation easier as we don't have to make copies of the dict to pass it to existing validations.

- branch:
  - condition:
      condition: state
      entity_id: input_binary.guest_mode
      state: 'on'
    sequence:
      - service: system_log.write
        data:
          message: There are guest

@OnFreund
Copy link
Author
OnFreund commented May 5, 2020

@balloob would the state machine be frozen between evaluation of the different condition clauses? Otherwise it wouldn't be possible to achieve atomic branching and ensure that one and only one branch is true.

@elupus
Copy link
elupus commented May 5, 2020

@OnFreund As long as there is no async await in the code path it will be frozen.

@PiotrMachowski
Copy link
PiotrMachowski commented May 5, 2020

I really like suggestion made by @balloob, it can be also extended to incorporate an "else" branch suggested by @thomasloven:

- branch:
  - condition:
      condition: state
      entity_id: input_select.some_value
      state: state_1
    sequence:
      - service: system_log.write
        data:
          message: State is one
  - condition:
      condition: state
      entity_id: input_select.some_value
      state: state_2
    sequence:
      - service: system_log.write
        data:
          message: State is two
  default_sequence:
    - service: system_log.write
      data:
        message: State has unexpected value

It would be something similar to switch statement and pattern matching known from programming languages.

@OnFreund
Copy link
Author
OnFreund commented May 5, 2020

@elupus cool, that should take care of conditions (I don't believe conditions can have side effects), but we need to ensure that the sequences don't run between the conditions, because those would most likely have awaits in them.
So first run all conditions in one go, then run sequences for conditions that evaluated to true.

@PiotrMachowski I really like the default_sequence, I think it opens up a lot of options, and also makes the binary case (one sequence for true, another one for false) really easy.

@PiotrMachowski
Copy link
PiotrMachowski commented May 5, 2020

So first run all conditions in one go, then run sequences for conditions that evaluated to true.

I think that it would be better to run only a sequence for a first passed condition (like mentioned above by @balloob)

@OnFreund
Copy link
Author
OnFreund commented May 5, 2020

Oh, if only the first true conditi 8000 on takes then you're right. (assuming conditions can indeed have no side effects).
In any case, I really like the default_sequence suggestion. I vote for the syntax in your comment.

@Jc2k
Copy link
Member
Jc2k commented May 5, 2020

Would this support nesting? Contrived example for syntax of what I mean.

- branch:
  - condition:
      condition: state
      entity_id: input_binary.guest_mode
      state: 'on'
    sequence:
    - branch:
      - condition:
          condition: state
          entity_id: binary_sensor.is_house_cold
          state: 'on'
        sequence:
          - service: system_log.write
            data:
              message: There are guests and it is cold

@thomasloven
Copy link

Most likely.

@pvizeli
Copy link
Member
pvizeli commented May 5, 2020

The default_sequence could be pretty hard to implement with how scripts work. I would suggest to start simple with the branch concept and later add the default. Step by Step

@OnFreund
Copy link
Author
OnFreund commented May 5, 2020

I believe default_sequence is the most important part of this (in fact, you could potentially implement everything else with it using nested branches).

@PiotrMachowski
Copy link

@pvizeli Could it be possible to make default_sequence internally just an alias for a condition that is always true?

@balloob
Copy link
Member
balloob commented May 5, 2020

if we're going to execute first branch to match, we could just execute first branch that has no condition specified.

And yes, these can be nested. A sequence doesn't know what it's executing, so it can be yet another branch. It won't be fun to keep track of which branch you're editing though 🤷

@OnFreund
Copy link
Author
OnFreund commented May 5, 2020

OK, sounds like we're converging to a good outcome.
An example of where we are now:

- branch:
  - condition:
      condition: state
      entity_id: input_select.some_value
      state: state_1
    sequence:
      - service: system_log.write
        data:
          message: State is one
  - condition:
      condition: state
      entity_id: input_select.some_value
      state: state_2
    sequence:
      - service: system_log.write
        data:
          message: State is two
      - branch:
        - condition:
            condition: state
            entity_id: binary_sensor.some_value
            state: 'on'
          sequence:
            - service: system_log.write
              data:
                message: Nested conditions work but are not encouraged
  - sequence: # no condition acts as a fallback - it's always true
    - service: system_log.write
      data:
        message: State has unexpected value
67E6

@thomasloven
Copy link

This would also pretty much require some way to stop a script execution, so you can stop after some branches, but continue after others.

Simplest way would be to add a

- condition: false

but that's not very user friendly.

@thomasloven
Copy link

My pr has been updated to work with the new syntax: home-assistant/core#35133

@OnFreund
Copy link
Author
OnFreund commented May 6, 2020

@thomasloven what if conditions break out of the current sequence, but don't stop script execution altogether? I'm not entirely sure about current behavior, but if the following two statements are true, then it's a backwards compatible solution:

  1. The only way to nest sequences today is by calling a separate script.
  2. A condition only breaks out of the currently executing script, not its caller.

@thomasloven
Copy link

I'm talking about the case:

if A:
  Do someting if A
elif B:
  Do something if B
else:
  Do something else
  exit

Do something common for A and B

Without the exit statement, the common action would need to be repeated in the A and B branches.

@OnFreund
Copy link
Author
OnFreund commented May 6, 2020

I think exit is a nice-to-have, but not strictly required. The "do something common" part can be achieved with repetition, like you mentioned.

@gjbadros

This comment has been minimized.

@thomasloven

This comment has been minimized.

@gjbadros

This comment has been minimized.

@OnFreund
Copy link
Author
OnFreund commented Jun 1, 2020

@gjbadros this is getting off topic. Maybe open a new arch issue?

@gjbadros
Copy link
gjbadros commented Jun 1, 2020

dropping it; my other arch issues are more important to me to get feedback on.

F438

@pnbruckner
Copy link

Coincidentally I started thinking about what to do next with scripts and was toying with some variations of an if construct (and others for looping) and ran across this conversation. I kind of prefer this over the more traditional if, especially since it fits better with YAML. I'd probably use default, though:

- choose:
  - condition:
    # One or more conditions that are AND'd
    sequence:
    # Sequence that executes if associated condition is first to evaluate to true
  - # Repeat the above as many times as desired
  default: # Optional
  # Sequence that executes if none of the conditions evaluate to true

I know it's a bit off topic, but I've started to work on the implementation of the following new features for looping:

- while:
  # One or more conditions that are AND'd
  sequence:
  # Sequence that executes repeatedly while above conditions evaluate to true

and:

- repeat: COUNT # template accepted here as well, rendered once each time script reaches this step
  sequence:
  # Sequence that executes repeatedly the number of times determined by repeat value above

Although one can do looping now with a single, parallel mode script, it actually causes recursion which is probably less than optimal. I like the idea of native looping constructs. I'll start on PRs for those unless the general consensus says otherwise. And @thomasloven, I'm happy to take over the work on this choose option if you like (again, assuming we've reached consensus here.)

@thomasloven
Copy link

RE: taking over choose: Please, feel free to do so. And feel free to build on home-assistant/core#35133 or to throw it all away and do it right instead.
The syntax you propose will not work, though, since it mixes lists and dictionaries - see comments marked as off-topic comments.

As mentioned in that PR, I also had some thoughts on looping and on script local variables.
Maybe that should be put in a new arch issue, though. Or this be renamed?

@pnbruckner
Copy link

The syntax you propose will not work, though, since it mixes lists and dictionaries

Each of the three I suggested above are perfectly valid. E.g., The "choose" step is a dictionary with two keys: choose & default, the value of each being a list. And the elements of the choose list are themselves dictionaries with two keys: condition & sequence, and the values for each of those are also lists. For "while", its a dictionary with two keys: while & sequence, again where the value of each is a list. And for "repeat", it's a dictionary with two keys: repeat & sequence. The value of repeat is a number or string (which can be a template), and the value of sequence is a list. In fact, I've already begun the implementation of repeat and it's working well with regards to the schema and created objects.

I definitely looked at what you did in 35133 and it was a very good start. In fact, I had originally discarded the idea of "sub-blocks", thinking the implementation didn't lend itself to that. But I obviously had blinders on! 😄 In my "repeat" implementation I'm using that idea -- i.e., creating another Script object for each sub-block. So thanks for that!

I'll review your PR again in more detail to better understand what you were thinking re: looping & local variables.

Regarding continuing and/or starting a new architecture topic, I leave that to others. I think this was a very good discussion, but it seems to have reached its natural conclusion. And for repeat & while, those seem straight forward enough that I think they can be handled via PRs. But that's just my opinion.

@OnFreund
Copy link
Author
OnFreund commented Jul 1, 2020

@pnbruckner thanks for taking this on, that's awesome!
A few notes:

  1. I want to make sure we don't lose some of the important finer points that are included in @thomasloven's MVP (e.g. this resolved conversation)
  2. I really like the use of if instead of condition, since each if clause is a list of condition clauses. I suggest we keep it - it's less confusing.
  3. I really like default but there was some concern about how feasible it is to implement. The following three comments also discuss this and the alternatives, in case it's not feasible.

@pnbruckner
Copy link
  1. Yes, I'm aware of the concern about atomicity. I will definitely keep that in mind as I attempt to implement these new features. Worst case there may be another way to address it. E.g., with something like this:
- choose: SEQUENCE
  sequence:
    abc:
    # Sequence
    def:
    # Sequence
    default:
    # Sequence

where SEQUENCE would typically be a template. Being a single template it would be evaluated atomically, and would specify which sequence to run. The only downside I see to this is it requires the user to understand templates, which many don't. On the other hand, this is sort of an advanced feature, so is that asking much?

  1. if or condition, I can see an argument for either. Doesn't really matter to me.

  2. I don't see the difficulty. Seems pretty straightforward to me.

@OnFreund
Copy link
Author
OnFreund commented Jul 1, 2020
  1. The implementation by @thomasloven managed to achieve atomicity, so I'd rather stick to that than have a cumbersome template and compare its evaluation to various values.
  2. Great, that's definitely my preferred syntax.

@pnbruckner
Copy link

There certainly is an advantage with the generally agreed upon approach. However, there's also an advantage to a single template. Yes, it will likely be relatively large, but all the "logic" would be in one place, and hence possibly easier to understand and maintain, rather than having that "logic" scattered across many, many lines, with each list of conditions possible fairly far apart from each other. Not necessarily the best for understand-ability/maintenance.

@OnFreund
Copy link
Author
OnFreund commented Jul 1, 2020

IDK, it feels like replacing switch statements in a general purpose programming language with a large regular expression and branching on its output :)

@pnbruckner
Copy link

In fact, it would be very similar to a switch statement -- one expression decides which case to run. Not sure why that would be a bad thing. It's definitely atomic, keeps logic in one place, makes it a bit more consistent with my "repeat" & "while" constructs (i.e., the second part is sequence), ...

But, I'm only suggesting this as an alternative if the previously agreed on "choose" construct doesn't work out for some reason.

@OnFreund
Copy link
Author
OnFreund commented Jul 1, 2020

Yeah, I get the one expression part, what I meant is that complex logic within a template would be akin to complex logic within a regular expression. Templates are great for simple stuff, but quickly become hard to follow when more logic is added.

Anyway, thanks again for picking this up - really appreciated.

@pnbruckner
Copy link

Again, I know it's a bit off topic, but I believe I have the implementation of the repeat loop completed. I, of course, still have to add tests and update the docs, but if anyone is interested see https://github.com/home-assistant/core/tree/script-repeat-loop.

@pnbruckner
Copy link
pnbruckner commented Jul 1, 2020

@OnFreund

The implementation by @thomasloven managed to achieve atomicity

That's actually not true. At least it doesn't appear so to me. The code that evaluates the conditions, although it does them all in a single loop, contains await statements, so I don't think it can be said to be guaranteed to be atomic.

If atomicity is important, then I think maybe my suggestion for using a single template in a switch style statement may be the way to go. And I don't agree with your assessment that that is less clear. E.g., compare:

- choose:
  - if:
    - condition: state
      entity_id: device_tracker.me
      state: office
    - condition: numeric_state
      entity_id: sensor.n_guests
      above: 0
    sequence:
    - service: switch.turn_on
      entity_id: switch.inside_lights
  - if:
    - condition: not
      conditions:
      - condition: state
        entity_id: device_tracker.me
        state: home
    - condition: numeric_state
      entity_id: sensor.luminance
      below: 50
    sequence:
    - service: switch.turn_on
      entity_id: switch.front_porch_light

to:

- choose: >
    {% if not is_state('device_tracker.me', 'home') and states('sensor.n_guests')|int > 0 %}
      not_home
    {% elif is_state('device_tracker.me', 'office') and states('sensor.luminance')|float < 50 %}
      office
    {% endif %}
  sequence:
    not_home:
    - service: switch.turn_on
      entity_id: switch.inside_lights
    office:
    - service: switch.turn_on
      entity_id: switch.front_porch_light

I can tell you which I think is easier to understand and maintain, and the decision making is atomic.

Yes, I understand the the first one could be changed to templates as well, and be more compact. The points are 1) whether the decision logic is in one place as opposed to scattered throughout the statement (which gets worse as there are more choices), and 2) whether atomicity can be achieved in the decision logic.

EDIT: BTW, I'm assuming rendering a template is an atomic operation in HA. I guess I don't know that is true.

@gjbadros
Copy link
gjbadros commented Jul 1, 2020 via email

@OnFreund
Copy link
Author
OnFreund commented Jul 2, 2020

That's actually not true. At least it doesn't appear so to me. The code that evaluates the conditions, although it does them all in a single loop, contains await statements, so I don't think it can be said to be guaranteed to be atomic.

If I'm reading this correctly, the await happens when you've found a True condition, after which there's an immediate break and no more conditions evaluate, so I believe it is atomic.

As for the template approach, don't get me wrong - for me, as an advanced user, it's great, and most of my use cases are indeed about a single expression and multiple potential values to compare against. However, I think the barrier to entry with it might be too high, especially when complex logic is required.

@thomasloven
Copy link

Each of the three I suggested above are perfectly valid

You are right, of course. Sorry.
RE: the looping and variables, that's not incorporated in that PR other than being mentioned as a future posibility in the description - so don't look too hard :).

RE: atomicity, there are two problems.
1: A condition being true when evaluated, but not when the branch is executed.
2: State changing during evaluation such that cases where at least one is guaranteed to be true are missed.

The first case probably can't be solved, and is also a "problem" for the normal condition.
The second case is what's adressed in the last commit of my PR in a way that minimizes its impact. It's not removed entirely, but I honestly don't think it's likely to ever be a problem. Even less so if there's a default branch.

Of course, an alternative approach could be to freeze a copy of the state machine at the beginning of _async_branch_step and use the copy for evaluating the conditions, but the real one for running the branch... I guess that would eliminate the second problem.

@OnFreund
Copy link
Author
OnFreund commented Jul 2, 2020

The second case is what's adressed in the last commit of my PR in a way that minimizes its impact. It's not removed entirely, but I honestly don't think it's likely to ever be a problem. Even less so if there's a default branch.

Can you elaborate on what remains non-atomic in this implementation?
(and I agree that 1 is not a concern, and is also true for normal conditions).

@thomasloven
Copy link

No, you are right. My mind was stuck in pre-emptive threaded execution for some reason.
Case 2 is solved in home-assistant/core#35133.

@pnbruckner
Copy link
pnbruckner commented Jul 2, 2020

Are you referring to this bit of code:

        conditions = [(
            await self._get_condition_config(b["if"]) if b.get("if") else None,
            b["sequence"]
            ) for b in branches]

If so, how is that atomic? There's an await in the loop.

Of course, an alternative approach could be to freeze a copy of the state machine at the beginning of _async_branch_step and use the copy for evaluating the conditions

Now that's an idea!

I personally wouldn't expect the state of the system to be the same between the condition evaluation and the branch execution. That's asking a bit much. I wouldn't worry about that.

minimizes its impact. It's not removed entirely, but I honestly don't think it's likely to ever be a problem

If there's one thing I've learned in over 40 years of embedded programming, if something can happen, it will happen. And this is exactly the sort of thing to make a system misbehave in mysterious ways that are next to impossible to debug/fix.

I'm still not necessarily saying I think it has to be a single template. I do agree that raises the bar for the average user. The idea about getting a frozen copy of the State Machine while evaluating the conditions is potentially a great way to solve the problem. Just not sure how easy that will be to implement since that would probably also affect the condition helper and who knows what else.

Having said all that, I'm wondering why this is considered a problem for this, but not for any other complex, compound set of conditions. If this is really a problem, wouldn't it be true for, say, an automation's condition section, too?

@OnFreund
Copy link
Author
OnFreund commented Jul 2, 2020

@pnbruckner that piece of code doesn't evaluate the conditions, but rather just gathers them. The evaluation happens here and is atomic (in the "between condition evaluations" sense, not "covering both conditions and sequences", which I agree isn't needed).

@pnbruckner
Copy link

Ah, ok, now I see. Thanks! I wasn't reading the code very closely. My bad! Yep, that makes perfect sense now.

@pnbruckner
Copy link

So just to make sure we're still all on the same page, the choose statement will look like this:

- choose:
  - if:
    - condition: ...
    sequence:
    - ...
  - if:
    - condition: ...
    sequence:
    - ...
  # "Default" case leaves out optional if part:
  - sequence:
    - ...

Is that right?

@OnFreund
Copy link
Author
OnFreund commented Jul 2, 2020

Perfect!

@pnbruckner
Copy link

Initial implementation complete: https://github.com/home-assistant/core/tree/script-choose-action. Still need to add test and update docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants
0