Sleep as Android webhooks handler

I recently discovered that the Sleep as Android sleep tracking app has a webhooks integration that works nicely with Home Assistant. I had previously been using Tasker to catch the SaA events and pass them along to HA via the REST API. It worked fine, but always felt a little hacky to me. This blueprint handles all the events that Sleep as Android puts out per their documentation. Though, I haven’t tested every single one because I don’t use most of them.

I also put in a feature to only run the automation if a person entity is in a specific zone. This would be useful while you’re on vacation and forget (or are just too lazy) to disable things manually.

In Sleep as Android, you can turn on the webhooks integration at:
Settings > Services > Automation > Webhooks

Use this as the URL:
https://<home-assistant-host>/api/webhook/<webhook-id>

for example:
https://ha.duckdns.org:8123/api/webhook/4d95d8b7-9860-4030-8b11-5bd80cb1d574

The webhook ID can be anything unique, but for security purposes, it should be long and randomly generated.

Some of the events are triggered several hundred times throughout the night. So you might want to consider filtering the automation from the HA logs to cut down on the pollution or disabling the high frequency events in SaA:
Settings > Services > Automation > Events

Open your Home Assistant instance and show the blueprint import dialog with a specific blueprint pre-filled.

blueprint:
  name: Sleep as Android webhook handler
  description: >
    Home Assistant blueprint for handling Sleep as Android webhooks events
    
    
    In Sleep as Android app, set Webhooks URL to:
    
    https://<home-assistant-host>/api/webhook/<webhook-id>
    
    
    Sleep as Android events: https://docs.sleep.urbandroid.org/services/automation.html#events
    
    Blueprint based on above documentation as of 2022-03-11
    
    
    Some events include additional data ('value1' and 'value2'). This blueprint makes
    them available as template variables 'value1' and 'value2'. 'event' is also
    available as a variable if needed.
    
    
    Note:
    
    Some Sleep as Android events can be triggered several hundred times per night. Specific
    events can be disabled in the app. Otherwise, filtering this automation from
    the logs may be warranted (https://www.home-assistant.io/integrations/recorder/#configure-filter)
  source_url: https://gist.github.com/psbaltar/a954ef9c8d280c0b816ad875827ac993
  domain: automation
  input:
    webhook:
      name: Webhook ID
      description: "Webhook ID"
    localonly:
      name: Local Only
      description: "Only accessible from the local network"
      default: false
      selector:
        boolean:
    person:
      name: Person (optional)
      description: "Person to track (optional if Zone Check not enabled)"
      default: []
      selector:
        entity:
          domain: person
    zone:
      name: Zone (optional)
      description: "Check if person is in this zone (optional if Zone Check not enabled)"
      default: []
      selector:
        entity:
          domain: zone
    zone_check:
      name: Zone Check
      description: "Enable to run automation only if Person is in Zone"
      default: false
      selector:
        boolean:
    sleep_tracking_started:
      name: sleep_tracking_started
      description: ""
      default: []
      selector:
        action: {}
    sleep_tracking_stopped:
      name: sleep_tracking_stopped
      description: ""
      default: []
      selector:
        action: {}
    sleep_tracking_paused:
      name: sleep_tracking_paused
      description: ""
      default: []
      selector:
        action: {}
    sleep_tracking_resumed:
      name: sleep_tracking_resumed
      description: ""
      default: []
      selector:
        action: {}
    alarm_snooze_clicked:
      name: alarm_snooze_clicked
      description: >
        You have snoozed a ringing alarm.
        
        
        We are sending the following values:
        
        - value1: UNIX timestamp of the alarm start time, example: "1582719660934"
        
        - value2: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    alarm_snooze_canceled:
      name: alarm_snooze_canceled
      description: >
        You have a canceled an alarm that is currently snoozed.
        
        
        We are sending the following values:
        
          - value1: UNIX timestamp of the alarm start time, example: "1582719660934"
          
          - value2: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    time_to_bed_alarm_alert:
      name: time_to_bed_alarm_alert
      description: >
        Fires when you get a bedtime notification.
        
        
        We are sending the following values:
        
          - value1: UNIX timestamp of the alarm start time (the alarm which triggered the bedtime notification, based on your ideal daily sleep income), example: "1582719660934"
      default: []
      selector:
        action: {}
    alarm_alert_start:
      name: alarm_alert_start
      description: >
        Fires when alarm starts.
        
        
        We are sending the following values:
        
          - value1: UNIX timestamp of the alarm start time, example: "1582719660934"
          
          - value2: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    alarm_alert_dismiss:
      name: alarm_alert_dismiss
      description: >
        Fires when you dismiss alarm (after you solve CAPTCHA, if it’s set).
        
        
        We are sending the following values:
        
          - value1: UNIX timestamp of the alarm start time, example: "1582719660934"
          
          - value2: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    alarm_skip_next:
      name: alarm_skip_next
      description: >
        Fires when you tap dismiss an alarm from notification before it actually rings.
        
        
        We are sending the following values:
        
          - value1: UNIX timestamp of the alarm start time, example: "1582719660934"
        
          - value2: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    before_alarm:
      name: show_skip_next_alarm
      description: >
        Note: As of 2023-05-10, the documentation is incorrect. The app sends 'before_alarm' instead of 'show_skip_next_alarm'. This blueprint handles it correctly, but leaves the displayed name to be consistent with the documentation
        
        
        Fires exactly 1 hour before the next alarm is triggered.
        
        
        value1: UNIX timestamp of the alarm start time, example: "1582719660934"
      default: []
      selector:
        action: {}
    rem:
      name: rem
      description: "Fires when we estimate the start of REM phase."
      default: []
      selector:
        action: {}
    smart_period:
      name: smart_period
      description: "Fires at the start of the smart period."
      default: []
      selector:
        action: {}
    before_smart_period:
      name: before_smart_period
      description: >
        Fires 45 minutes before the start of smart period.
        
        
        We are sending the following value:
        
          - value: alarm label, example: "label" (Any tabs and newline characters in the label will be removed before sending)
      default: []
      selector:
        action: {}
    lullaby_start:
      name: lullaby_start
      description: "Fires when lullaby starts playing."
      default: []
      selector:
        action: {}
    lullaby_stop:
      name: lullaby_stop
      description: "Fires when lullaby is stopped (either manually or automatically)."
      default: []
      selector:
        action: {}
    lullaby_volume_down:
      name: lullaby_volume_down
      description: "Fires when we detect you fell asleep and starting lowering the volume of lullabies."
      default: []
      selector:
        action: {}
    deep_sleep:
      name: deep_sleep
      description: "Fires when we detect you going into deep sleep phase. Warning: This may result in lots of events during the night and may not exactly fit the resulting sleep graph as we can only detect phases reliably from whole-night data."
      default: []
      selector:
        action: {}
    light_sleep:
      name: light_sleep
      description: "Fires when we detect you going into light sleep phase. Warning: This may result in lots of events during the night and may not exactly fit the resulting sleep graph as we can only detect phases reliably from whole-night data."
      default: []
      selector:
        action: {}
    awake:
      name: awake
      description: "Fires when we detect you woke up."
      default: []
      selector:
        action: {}
    not_awake:
      name: not_awake
      description: "Fires when we detect you fell asleep."
      default: []
      selector:
        action: {}
    apnea_alarm:
      name: apnea_alarm
      description: "Fires when we detect a significant dip in your oxygen levels."
      default: []
      selector:
        action: {}
    antisnoring:
      name: antisnoring
      description: "Fires when antisnoring is triggered."
      default: []
      selector:
        action: {}
    sound_event_snore:
      name: sound_event_snore
      description: "Fires when we detect snoring."
      default: []
      selector:
        action: {}
    sound_event_talk:
      name: sound_event_talk
      description: "Fires when we detect talking."
      default: []
      selector:
        action: {}
    sound_event_cough:
      name: sound_event_cough
      description: "Fires when we detect coughing."
      default: []
      selector:
        action: {}
    sound_event_baby:
      name: sound_event_baby
      description: "Fires when we detect baby cry."
      default: []
      selector:
        action: {}
    sound_event_laugh:
      name: sound_event_laugh
      description: "Fires when we detect laughter."
      default: []
      selector:
        action: {}
mode: parallel
variables:
  var_zone_check: !input zone_check
trigger:
  - platform: webhook
    webhook_id: !input webhook
    allowed_methods:
      - POST
    local_only: !input localonly
condition:
  condition: or
  conditions:
    - "{{ not var_zone_check }}"
    - condition: and
      conditions:
        - "{{ var_zone_check }}"
        - condition: zone
          entity_id: !input person
          zone: !input zone
action:
  - variables:
      event: "{{ trigger.json.event }}"
      value1: "{{ trigger.json.value1 if trigger.json.value1 is defined }}"
      value2: "{{ trigger.json.value2 if trigger.json.value2 is defined }}"
  - choose:
      - conditions: "{{ event == 'sleep_tracking_started' }}"
        sequence: !input sleep_tracking_started
      - conditions: "{{ event == 'sleep_tracking_stopped' }}"
        sequence: !input sleep_tracking_stopped
      - conditions: "{{ event == 'sleep_tracking_paused' }}"
        sequence: !input sleep_tracking_paused
      - conditions: "{{ event == 'sleep_tracking_resumed' }}"
        sequence: !input sleep_tracking_resumed
      - conditions: "{{ event == 'alarm_snooze_clicked' }}"
        sequence: !input alarm_snooze_clicked
      - conditions: "{{ event == 'alarm_snooze_canceled' }}"
        sequence: !input alarm_snooze_canceled
      - conditions: "{{ event == 'time_to_bed_alarm_alert' }}"
        sequence: !input time_to_bed_alarm_alert
      - conditions: "{{ event == 'alarm_alert_start' }}"
        sequence: !input alarm_alert_start
      - conditions: "{{ event == 'alarm_alert_dismiss' }}"
        sequence: !input alarm_alert_dismiss
      - conditions: "{{ event == 'alarm_skip_next' }}"
        sequence: !input alarm_skip_next
      - conditions: "{{ event == 'before_alarm' }}" # documenation incorrctly calls this show_skip_next_alarm
        sequence: !input before_alarm
      - conditions: "{{ event == 'rem' }}"
        sequence: !input rem
      - conditions: "{{ event == 'smart_period' }}"
        sequence: !input smart_period
      - conditions: "{{ event == 'before_smart_period' }}"
        sequence: !input before_smart_period
      - conditions: "{{ event == 'lullaby_start' }}"
        sequence: !input lullaby_start
      - conditions: "{{ event == 'lullaby_stop' }}"
        sequence: !input lullaby_stop
      - conditions: "{{ event == 'lullaby_volume_down' }}"
        sequence: !input lullaby_volume_down
      - conditions: "{{ event == 'deep_sleep' }}"
        sequence: !input deep_sleep
      - conditions: "{{ event == 'light_sleep' }}"
        sequence: !input light_sleep
      - conditions: "{{ event == 'awake' }}"
        sequence: !input awake
      - conditions: "{{ event == 'not_awake' }}"
        sequence: !input not_awake
      - conditions: "{{ event == 'apnea_alarm' }}"
        sequence: !input apnea_alarm
      - conditions: "{{ event == 'antisnoring' }}"
        sequence: !input antisnoring
      - conditions: "{{ event == 'sound_event_snore' }}"
        sequence: !input sound_event_snore
      - conditions: "{{ event == 'sound_event_talk' }}"
        sequence: !input sound_event_talk
      - conditions: "{{ event == 'sound_event_cough' }}"
        sequence: !input sound_event_cough
      - conditions: "{{ event == 'sound_event_baby' }}"
        sequence: !input sound_event_baby
      - conditions: "{{ event == 'sound_event_laugh' }}"
        sequence: !input sound_event_laugh
    default:
      - service: persistent_notification.create
        data:
          title: "Sleep As Android blueprint exception"
          message: "Caught unhandled event: {{ event }}"
          notification_id: 'sleep_default_action_notification'
      - service: system_log.write
        data:
          level: warning
          message: "Sleep As Android blueprint caught unhandled event: {{ event }}"


Changes:
2023-05-10 - Updated blueprint to be compliant with new webhook scheme (changed in 2023.5.0); added “Local Only” option. Also corrected show_skip_next_alarm to before_alarm per reports in this thread.

5 Likes

Hi,

Firstly have to thank you for nice blueprint.
But how do i save webhook to secrets?
I get only error 500 when trying to save automation from blueprint and found on log that it has something to do with webhook id not saving in secrets.

I ran a quick test and couldn’t get it working either. I noticed that the script UI is making the webhook input a string. So when I try to use a secret, it gets saved as ‘!secret webhook-id’ (with quotes). I didn’t look through the logs, but I assume this prevents it from substituting the actual saved secret.

Unfortunately, I don’t know what to do about that. Hopefully someone else has some ideas.

just registerd myself to thank you for the blueprint

1 Like

Works great!
But there seems to be an error in the documentation and therefore in this blueprint.
show_skip_next_alarm is not called an hour before the alarm.
It now seems to be called: before_alarm.
When using that event name all seems to work!

1 Like

I can confirm that this is working, thanks @jeroen-wj!

Got a notification that said “Caught unhandled event: before_alarm

With the changes to Webhooks handling introduced in 2023.5 does anyone know how to edit the settings for the webhook to be set to ‘Only accessible from the local network’?

1 Like

I’ve updated the blueprint to include the local only option.

Thanks for letting me know. The update also corrects show_skip_next_alarm to before_alarm. Also thanks to @C-son for the verification. I haven’t tested it myself, but I double checked my changes, so it should be working.

Looks like there’s a new event alarm_rescheduled, which is not handled by this blueprint and spams annoying notifications every day :slight_smile:
I’ve forked gist to add this event to list.
kotborealis/sleep_as_android_webhooks.yaml

1 Like