Skip to content

Comparing Pyscript to Home Assistant Automations

Daniel Lashua edited this page Nov 13, 2020 · 9 revisions

Triggers

State Trigger

In Home Assistant:

- alias: some automation
  trigger:
    platform: state
    entity_id: binary_sensor.test
    to: 'on'
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In Pyscript:

@state_trigger('binary_sensor.test == "on"')
def turn_on():
  switch.test.turn_on()

State Trigger with "from"

In Home Assistant:

- alias: some automation
  trigger:
    platform: state
    entity_id: binary_sensor.test
    to: 'on'
    from: 'off'
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In Pyscript:

@state_trigger('binary_sensor.test == "on" and binary_sensor.test.old == "off"')
def turn_on():
  switch.test.turn_on()

State Trigger without "to" or "from"

In Home Assistant:

- alias: some automation
  trigger:
    platform: state
    entity_id: binary_sensor.test
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In Pyscript:

@state_trigger('binary_sensor.test')
def turn_on():
  switch.test.turn_on()

Template Trigger

In Home Assistant:

- alias: some automation
  trigger:
    platform: template
    value_template: "{{ is_state('binary_sensor.test', 'on') }}"
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In Pyscript:

@state_trigger('binary_sensor.test == "on"')
def turn_on():
  switch.test.turn_on()

Conditions

Template Condition

In Home Assistant:

- alias: some automation
  trigger:
    platform: state
    entity_id: binary_sensor.test
    to: "on"
  condition:
    condition: template
    value_template: "{{ is_state('input_boolean.test', 'on') }}"
  action:
    - service: homeassistant.turn_on
      entity_id: switch.test

In Pyscript:

@state_trigger('binary_sensor.test == "on"')
@state_active('input_boolean.test == "on"')
def turn_on():
  switch.test.turn_on()

Full Examples

Alert Replacement

The BuiltIn Alerts in Home Assistant require a Home Assistant restart to activate and don't have all the features I'd like. So, outside of basic cases, I often perform alerts with a Home Assistant Automation like this:

- alias: dishwasher_done_notification
  mode: single
  trigger:
    - platform: template
      value_template: >
        {{
          is_state('input_select.dishwasher_status', 'clean')
        }}
    - platform: homeassistant
      event: start
    - platform: event
      event_type: automation_reloaded
  condition:
    - condition: template
      value_template: >
        {{
          is_state('input_select.dishwasher_status', 'clean')
        }}
  variables:
    start_time: "{{ as_timestamp(now()) }}"
  action:
    - repeat:
        sequence:
          - variables:
              waited: "{{ ( (as_timestamp(now()) - start_time|float) / 60 )|round }}"
          - choose:
              conditions:
                - condition: template
                  value_template: >
                    {{
                        repeat.first
                    }}
              sequence:

                - service: notify.house_notify_script
                  data:
                    message: "The dish washer is done. Please empty it."

            default:
              - service: notify.house_notify_script
                data:
                  message: "The dish washer has been done for {{ waited }} minutes. I've told you {{ repeat.index }} times. Please empty it."

          - delay:
              minutes: 30

        until:
          - condition: template
            value_template: >
              {{
                not is_state('input_select.dishwasher_status', 'clean')
              }}

Every time I need a new alert like this, I cut and paste the automation and replace all the names, entities, messages, and conditions. If I, later, make an improvement on this automation, I have to change it in every place that I've used it already. Making it "reusable" by writing it as a Home Assistant Script is cumbersome because of the condition templates, and, even then, I'd still need an automation to trigger it.

Writing it in Pyscript, however, allows me to easily reuse the code. We can also take advantage of Pyscript's persistence feature to have an alert keep it's state through a Home Assistant Restart.

The reusable Pyscript might look like this. You'll notice I'm setting my alert parameters directly in the code. However, this could also be turned into an App with parameters being set in YAML. This Pyscript is about twice as long as the original Home Assistant automation. But, I'm using a verbose syntax to make the code more readable. And, remember, this is reusable and has persistence. If "lines of code" in an automation is important to you, you only need two of these to "break even".

import time


registered_triggers = []

def make_alert(config):
    pass

    log.info(f'Loading Alert {config["name"]}')

    alert_entity = f'pyscript.alert_{config["name"]}'
    state.persist(
        alert_entity,
        default_value="off",
        default_attributes={
            "count": 0,
            "start_ts": 0
        }
    )

    @task_unique(f'alert_{config["name"]}')
    @state_trigger(f'True or {config["condition"]}')
    @time_trigger('startup')
    def alert():
        condition_met = eval(config['condition'])
        if not condition_met:
            state.set(
                alert_entity,
                "off",
                count=0,
                start_ts=0
            )
            return
        
        log.info(f'Alert {config["name"]} Started')

        interval_seconds = config['interval'] * 60

        try: 
            alert_count = int(state.get(f'{alert_entity}.count'))
            alert_start_ts = int(state.get(f'{alert_entity}.start_ts'))
        except:
            alert_count = 0
            alert_start_ts = 0

        if alert_start_ts == 0:
            alert_start_ts = round(time.time())


        while condition_met:
            alert_count = alert_count + 1
            alert_time_seconds = time.time() - alert_start_ts
            alert_time = round(alert_time_seconds / 60)

            state.set(alert_entity, "on", start_ts=alert_start_ts, count=alert_count)

            message_tpl = config['message']
            if alert_count > 1 and "message_more" in config:
                message_tpl = config['message_more']

            message = eval(f"f'{message_tpl}'")

            if message:
                log.info(f'Sending Message: {message}')
                service.call(
                    "notify",
                    config["notifier"],
                    message=message
                )

            wait = task.wait_until(
                state_trigger=f'not ({config["condition"]})',
                timeout = interval_seconds,
                state_check_now=True
            )

            if wait['trigger_type'] == 'state':
                condition_met = False

        state.set(
            alert_entity,
            "off",
            count=0,
            start_ts=0
        )
        log.info(f'Alert {config["name"]} Finished')


    registered_triggers.append(alert)


@time_trigger('startup')
def alert_startup():
    make_alert({
        "name": "dishwasher_done",
        "condition": "input_select.dishwasher_status == 'clean'",
        "interval": 30,
        "notifier": "house_notify_script",
        "message": "The dishwasher is done. Please empty it.",
        "message_more": "This dishwasher has been done for {alert_time} minutes. I have told you {alert_count} times already. Please empty it."
    })

You can see an even more complete version of alert written as an app.