ROAST My Blueprint! (feedback on how to be more elegant)

So this would also apply to standard automations, but as blueprints are the new hotness I am trying to learn from the Jedi-masters on here as to what’s efficient.

Roast away! Note this blueprint works, being used in 14 instances in my system that pass the WAF.

Note I am not fond of the redundant triggers but have not found a way to get the same as “for” in a trigger without feeling bulky.

Note2: The 3rd trigger is a timeout to auto-turn off lights after a long period; typically these are set to 240 minutes or so and to catch if kids manually turn something on.

Note3: The light_off is different as most of my rooms have a light group; so this automation turns on the preferred light in that group on motion; but when the motion clears and room assumed empty it turns off the group of lights for the room.

Thanks!

blueprint:
  name: Motion activated Light Time Constraints
  description: Turn on a light when motion is detected.
  domain: automation
  input:
    motion_trigger:
    light_on:
    light_off:
    wait_time:
    timeout_time:
    after_time:
    before_time:
    illumination_cutoff:
    blocker_entity:

mode: restart

trigger:
  - platform: state
    entity_id: !input motion_trigger
    from: "off"
    to: "on"

  - platform: state
    entity_id: !input motion_trigger
    from: "on"
    to: "off"
    for:
      minutes: !input wait_time

  - platform: state
    entity_id: !input light_off
    to: "on"
    for:
      minutes: !input timeout_time
  
variables:
  light_on: !input light_on
  light_off: !input light_off
  motion_trigger: !input motion_trigger
  illumination_cutoff: !input illumination_cutoff
  after_time: !input after_time
  before_time: !input before_time
  blocker_entity: !input blocker_entity

action:
  - choose:
      - conditions:
          - "{{ trigger.to_state.state == 'on' }}"
          - "{{ trigger.entity_id == motion_trigger }}"
          - "{{ (blocker_entity == false) or (is_state(blocker_entity,'off')) }}"
          - "{{ (before_time == false and after_time == false) or (before_time != false and after_time != false and after_time < now().strftime('%H:%M') < before_time) }}"
          - "{{ (states('sensor.ms_family_room_luminance')|float < illumination_cutoff|float) }}"

        sequence:
          - choose:
              - conditions:
                  - "{{ states[light_on].domain == 'light' }}"
                sequence:
                  - service: light.turn_on
                    data:
                      entity_id: !input light_on
                      transition: 5
                      brightness_pct: "{{ states('sensor.light_brightness_target') }}"
                      
              - conditions:
                  - "{{ states[light_on].domain != 'light' }}"
                sequence: 
                  - service: homeassistant.turn_on
                    entity_id: !input light_on

      - conditions:
          - "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
        sequence:
          - service: homeassistant.turn_off
            entity_id: !input light_off

The most elegant way to start any blueprint is to have a robust, tested, working automation.
Then convert that to a blueprint.

Attempting to create a blueprint direct means you have to keep track of variables, logic interactions (karnaugh maps/truth tables), sequences, and the translations (to and from automations and blueprints).
Why make it hard for yourself ?
A grasp of the automation basics will serve in all circumstances.
Ie show us what you wish to achieve and work back from there

User Acceptance Testing


As for code’s quality, it looks fine to me.

2 Likes

I was really hoping you and @mf_social would have some ways to clean it up. Those first two triggers bug me but seems the cleanest way to use the for.

@Mutt this did start out as automations, 14 of them being quite similar, so the move to a blueprint was to get more reusability. That’s why things like the time constraint as well as the override are optional. It’s my only blueprint as the only repetitive automations in my system; I don’t see the point of using a blueprint if only a single automation is going to use it.

2 Likes

What’s the point of this…

states[light_on].domain

In the choose statements?

Surely its always a light? :man_shrugging:

Edit - that being the case, as a first once-over it could be reduced to this…

trigger:
  - platform: state
    entity_id: !input motion_trigger
    to: "on"
  - platform: state
    entity_id: !input motion_trigger
    to: "off"
    for:
      minutes: !input wait_time
  - platform: state
    entity_id: !input light_off
    to: "on"
    for:
      minutes: !input timeout_time
variables:
  light_on: !input light_on
  light_off: !input light_off
  motion_trigger: !input motion_trigger
  illumination_cutoff: !input illumination_cutoff
  after_time: !input after_time
  before_time: !input before_time
  blocker_entity: !input blocker_entity
action:
  - choose:
      - conditions:
          - "{{ trigger.to_state.state == 'on' }}"
          - "{{ trigger.entity_id == motion_trigger }}"
          - "{{ (blocker_entity == false) or (is_state(blocker_entity,'off')) }}"
          - "{{ (before_time == false and after_time == false) or (before_time != false and after_time != false and after_time < now().strftime('%H:%M') < before_time) }}"
          - "{{ (states('sensor.ms_family_room_luminance')|float < illumination_cutoff|float) }}"
        sequence:
          - service: light.turn_on
            data:
              entity_id: !input light_on
              transition: 5
              brightness_pct: "{{ states('sensor.light_brightness_target') }}"
      - conditions:
          - "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
        sequence:
          - service: homeassistant.turn_off
            entity_id: !input light_off

But I agree with @123, it’s generally quite well structured. The triggers are what they are, they can’t be reduced any more. I think all the variables are needed because they are all needed for templates, and the choose decides whether the light goes on or off, and because you’re passing brightness and transition with the turn on you can’t use a service template there, so choose is the best option.

I’ll be honest, I’m not properly ‘in the game’ today so I didn’t really double check the templates, but if the only other option after the first choose is the second one, then that should be a default rather than another condition, but again that’s just nit-picking really.

I’m afraid I must confess I skimmed your post, the ‘preformatted’ code window shows a limited window without scrolling
I appologise.
Regardless given your previous contributions I should have known better, sorry

Could also be a switch, and they don’t take the brightness_pct and transition arguments, so I had to have the two paths.

I also tried to use the 2nd condition in the inner choose as a default but it kept complaining and figured there might be an issue with nested choose’s with a default.

No worries, I appreciated any and all feedback as many on here, you included, have really wonderfully elegant yaml that I try to learn and duplicate.

Less lines of yaml make me happy as long as I can still understand it in 6 months :wink:

I replaced it with default and the resulting blueprint passed Check Configuration and Reload Automations without complaint and was used to generate an automation.

Relevant portion is here:

action:
  - choose:
      - conditions:
          - "{{ trigger.to_state.state == 'on' }}"
          - "{{ trigger.entity_id == motion_trigger }}"
          - "{{ (blocker_entity == false) or (is_state(blocker_entity,'off')) }}"
          - "{{ (before_time == false and after_time == false) or (before_time != false and after_time != false and after_time < now().strftime('%H:%M') < before_time) }}"
          - "{{ (states('sensor.ms_family_room_luminance')|float < illumination_cutoff|float) }}"
        sequence:
          - choose:
              - conditions:
                  - "{{ states[light_on].domain == 'light' }}"
                sequence:
                  - service: light.turn_on
                    data:
                      entity_id: !input light_on
                      transition: 5
                      brightness_pct: "{{ states('sensor.light_brightness_target') }}"
            default:
              - service: homeassistant.turn_on
                entity_id: !input light_on
      - conditions:
          - "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
        sequence:
          - service: homeassistant.turn_off
            entity_id: !input light_off

The resulting automation looks like this (just to prove it succeeded as far as this step):

alias: Motion activated Light Time Constraints - test
description: ''
use_blueprint:
  path: local/Time constrained motion activated lights.yaml
  input:
    motion_trigger: binary_sensor.whatever
    light_on: light.whatever
    light_off: light.whatever
    wait_time: '30'
    timeout_time: '60'
    after_time: '17:30'
    before_time: '16:30'
    blocker_entity: input_boolean.whatever
    illumination_cutoff: '50'

Obviously I can’t test it because I’ve supplied it with bogus entities and your automation contains two hard-coded sensor entities.

1 Like

FWIW, I recently adopted a different formatting convention for my automations. In a nutshell, wherever indenting is not necessary, then I do not indent. It’s a stylistic choice that results in the code remaining closer to the left margin rather than being pushed to the right.

Here’s the convention used for lists that I had followed; note the two-space indent before each hyphen.

action:
  - choose:
      - conditions:
           - "{{ whatever }}"

Here’s what I do now:

action:
- choose:
  - conditions:
    - "{{ whatever }}"

Here’s the action portion of your automation using this convention. Compare it to the example in my previous post (above) to see how the code (especially the nested conditions) remains much closer to the left, and in full view, as opposed to being pushed out of view to the right.

action:
- choose:
  - conditions:
    - "{{ trigger.to_state.state == 'on' }}"
    - "{{ trigger.entity_id == motion_trigger }}"
    - "{{ (blocker_entity == false) or (is_state(blocker_entity,'off')) }}"
    - "{{ (before_time == false and after_time == false) or (before_time != false and after_time != false and after_time < now().strftime('%H:%M') < before_time) }}"
    - "{{ (states('sensor.ms_family_room_luminance')|float < illumination_cutoff|float) }}"
    sequence:
    - choose:
      - conditions: "{{ states[light_on].domain == 'light' }}"
        sequence:
        - service: light.turn_on
          data:
            entity_id: !input light_on
            transition: 5
            brightness_pct: "{{ states('sensor.light_brightness_target') }}"
      default:
      - service: homeassistant.turn_on
        entity_id: !input light_on
  - conditions: "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
    sequence:
    - service: homeassistant.turn_off
      entity_id: !input light_off

Also, you can do this:

  - conditions: "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
    sequence:

in lieu of this (when there’s only one Template Condition):

  - conditions: 
    - "{{ (trigger.to_state.state == 'off') or (trigger.entity_id == light_off) }}"
    sequence:
1 Like

Thanks, I must have had the default not aligned properly. Your example works fine.

Also thanks for the tip on the inline shorthand conditions.

Edit - Spoke too soon, didn’t like the default. Can’t figure it out; but changing to a 2nd condition works fine.

Error is:

Blueprint Motion activated Light Time Constraints generated invalid automation with inputs OrderedDict([('motion_trigger', 'binary_sensor.master_closet_pir_sensor'), ('light_on', 'switch.master_closet_switch'), ('light_off', 'switch.master_closet_switch'), ('wait_time', 15), ('timeout_time', 30), ('after_time', '07:00:00'), ('before_time', '20:30:00'), ('illumination_cutoff', 180), ('blocker_entity', False)]): extra keys not allowed @ data['action'][0]['choose'][0]['sequence'][0]['choose'][0]['default']. Got None