Custom Component: Unifi Protect

The thumbnail is created by the controller once the event has finished - your automation triggers when motion starts, so longer motion events will outlast your 10 second delay and the thumbnail will not be available.

Still yet to refine my current G4 automation to send smart object detection events when first triggered, but the current state (including dynamic file name for the notification) is below in case it helps.

Notes:

  1. I use the event score and length to provide additional filtering of motion events unless the motion is a smart detection of an object.
  2. I trigger on the end of the motion event to ensure that the thumbnail is available (only seems to 404 when it’s raining). I’m planning to move the person / vehicle smart detections to be triggered on the start of a motion event and captured with a snapshot, but haven’t got round to it yet.
  3. On HA startup I load sensors with local and remote image paths from my secrets.yaml for easy maintenance.
  4. I use the original driveway_motion.jpg to provide a picture entity in my dashboard for the last motion event, and send the copied image with a dynamic file name as the notification. Images are cleaned up with a maintenance automation that runs a command line removal of images over 14 days old.
  5. The camera always records but whether we are notified or not depends on input_boolean.notify_driveway which is set by time of day or through the dashboard.
  6. /lovelace/camera_driveway is a hidden Lovelace view / tab with a panel mode live view picture entity showing the driveway camera.
  7. Notification is sent to the official Android companion app.
- id: unifiprotect_driveway_motion_notification
  alias: "Driveway Motion Notification"
  mode: queued
  trigger:
    platform: state
    entity_id: binary_sensor.motion_driveway
    from: "on"
    to: "off"
  condition:
    condition: and
    conditions:
      - condition: template
        value_template: >
           {{ ((is_state('sun.sun', 'above_horizon') and (state_attr('binary_sensor.motion_driveway', 'event_score') | int >= 50)))
             or ((is_state('sun.sun', 'below_horizon') and (state_attr('binary_sensor.motion_driveway', 'event_score') | int >= 50)))
             or (not is_state_attr('binary_sensor.motion_driveway', 'event_object', 'None Identified')) }}
      - condition: template
        value_template: >
           {{ (is_state('sun.sun', 'above_horizon') and (state_attr('binary_sensor.motion_driveway', 'event_length') | int >= 6))
               or (is_state('sun.sun', 'below_horizon') and (state_attr('binary_sensor.motion_driveway', 'event_length') | int >= 12))
               or (not is_state_attr('binary_sensor.motion_driveway', 'event_object', 'None Identified')) }}
  action:
    - variables:
        notify_image: "driveway_motion_{{ now().strftime('%Y%m%d%H%M') }}.jpg"
        motion_type: "{{ 'Motion' if is_state_attr('binary_sensor.motion_driveway', 'event_object', 'None Identified') else (state_attr('binary_sensor.motion_driveway', 'event_object') | capitalize) }}"
        motion_score: "{{ state_attr('binary_sensor.motion_driveway', 'event_score') }}"
        motion_length: >
          {% set time = (state_attr('binary_sensor.motion_driveway', 'event_length') | int) %}
          {% set mins = ((time % 3600) / 60) | int %}
          {% set secs = time - (mins * 60) %}
          {% if time < 60 %}{{ secs }} seconds
          {% else %}{{ mins }} minutes {{ secs }} seconds
          {% endif %}
    - service: input_datetime.set_datetime
      data:
        entity_id: input_datetime.last_driveway_motion
        time: "{{ (as_timestamp(now()) | timestamp_custom('%H:%M:%S', true)) }}"
        date: "{{ (as_timestamp(now()) | timestamp_custom('%Y-%m-%d', true)) }}"
    - choose:
        - conditions:
            # no object type detected, just motion
            - condition: template
              value_template: "{{ not is_state('sensor.motion_recording_driveway', 'never') }}"
            - condition: template 
              value_template: "{{ is_state_attr('binary_sensor.motion_driveway', 'event_object', 'None Identified') }}"
          sequence:
            - delay: "00:00:05"
            - condition: state
              entity_id: binary_sensor.motion_driveway
              state: "off"
            - service: unifiprotect.save_thumbnail_image
              data:
                entity_id: camera.driveway
                filename: "{{ states('sensor.snapshot_local') }}driveway_motion.jpg"
                image_width: 2688
        - conditions:
            # object detected (person, etc) - don't specify image width
            - condition: template
              value_template: "{{ not is_state('sensor.motion_recording_driveway', 'never') }}"
          sequence:
            - delay: "00:00:05"
            - condition: state
              entity_id: binary_sensor.motion_driveway
              state: "off"
            - service: unifiprotect.save_thumbnail_image
              data:
                entity_id: camera.driveway
                filename: "{{ states('sensor.snapshot_local') }}driveway_motion.jpg"
      default:
        - service: camera.snapshot
          data: 
            entity_id: camera.driveway
            filename: "{{ states('sensor.snapshot_local') }}driveway_motion.jpg"
    - service: shell_command.copy_file
      data:
        src_file: "{{ states('sensor.snapshot_local') }}driveway_motion.jpg"  
        dst_file: "{{ states('sensor.snapshot_local') }}{{ notify_image }}"
    - choose:
        - conditions:
            - condition: and 
              conditions:
                - condition: state
                  entity_id: input_boolean.notify_driveway
                  state: "on"
                - condition: state
                  entity_id:
                    - input_boolean.holiday_mode
                    - input_boolean.notify_via_sms
                  state: "off"
          sequence:                         
            - service: notify.all_mobile
              data:
                title: "Driveway Alert"
                message: >
                  {{ motion_type }} at {{ as_timestamp(state_attr('binary_sensor.motion_driveway', 'last_tripped_time')) | timestamp_custom('%H:%M') }}.<br>
                  Score: {{ motion_score }}. <br>
                  Length: {{ motion_length }}.
                data:
                  image: "{{ states('sensor.snapshot_remote') }}{{ notify_image }}"
                  clickAction: "/lovelace/camera_driveway"
                  group: Driveway

1 Like

Super thanks! I updated the code with camera.snapshot and so far so good :slight_smile: Will look into the code for file naming.

Wow! Your code was like finding a big safe full of money, you get super excited but not sure what to do with it :grin: So much useful stuff in there but it’s gonna take some time for me to digest it :slight_smile: I had no clue you can get all kind of information from the motion triggering. Is there a list somewhere with the variables that can be used?

Thank you so much for sharing!

I tend to use the homeassistant Developer Tools > State page to review entities and attributes and always build and test my template syntax in Developer Tools > Template. I do (still) build my automations in flat yaml files rather than using the user interface tools though as I find it easier to do things that way.

1 Like

Smart, I’ll be better at looking there.

Can I ask you (or someone else) how you deal with privacy and the images that are created? As it is now, they are stored in config/www and can be accessed by anyone. Is there a way to secure them and make them unreachable (I guess outside of www but still make them show up in the notification?

Hmm - can’t say I’d ever realised that they could be accessed on the internal http link without authenticating via HA first but based on a quick test they can (if someone knows the full image path).
Looks like this as come up in discussion not too long ago: What the heck: config/www == local? - #25 by tom_l with the suggestion of using the new /media path for storage (I’ve not tried this).

I may consider moving mine down the road but I’ve a lot of items on my todo list at the moment - moving from a core venv install to core in docker, migrating from zwave to zwavejs.
Changing image storage location on my install is likely to be a Saturday grepping session when I’ve got docker stuff done…

If you get anything working, please share.

A workaround would be to instead of showing a static image, a live stream would show in the notification. Would be so much useful, but I’m not having any luck getting it up and running.

@gr4z, I noticed that you struggeled to get the live stream in a notification, did you ever get that to work?

Edit:
I got the /media folder to work when sending notifications. This way the images aren’t accessible if someone has the direct link. A bit more secure. This page helped me get it to work:

1 Like

This probably explains the issue I am experiencing. I seem to get outdated images, and even after removing the files from the expected storage location and re-triggering the motion event, I get an old image. This was very confusing to me until reading this post.

I’m using the Home Assistant app on iOS, and need to figure out how to clear the cache for it. I am especially interested in doing it programmatically as part of my Node-Red flow that pops up a camera snapshot.

The easiest way to resolve image caching is to append a timestamp to the end of the captured image, making it unique. I work in YAML so set a variable with the image name as a first action in the automation and then reference that variable for the other actions.
I use a nightly cron job to clean images older than 14 days from the storage folder.

That link proved very useful - I’ve moved my notification images to a sub-directory of /media after testing it out as part of my prep to move to docker.
As part of the move I also tidied up my automations to use a folder_watcher to automatically update the last motion event image for the dashboard which removed the need for a file copy in the camera automation.
Just the venv->docker and zwave->zwavejs move to go now (all tested, just need a window to migrate with the time to bugfix if needed).

Hello all, clean install of HA and working very well, however im not able to find Unifi protect when i search for it under integrations. Any idea?

Have you installed the integration files using HACs or another method per https://github.com/briis/unifiprotect#installation?
The integration is not yet one of the core integrations included with a installation of HA.

Ah thank you :slight_smile:

Thanks so much for this! Your component is why I dug in and started using Home Assistant (as of yesterday) I have a newb question if you don’t mind. Everything seems to be working great except I notice a good 10 second delay on the feed with this method. Is there any way to improve on that?

alias: Doorbell Nest Display
description: ''
trigger:
  - platform: state
    entity_id: binary_sensor.motion_front_door
    to: 'on'
condition: []
action:
  - data:
      media_player: media_player.kitchen_display
    service: camera.play_stream
    entity_id: camera.front_door
  - delay:
      hours: 0
      minutes: 0
      seconds: 20
      milliseconds: 0
  - service: media_player.turn_off
    data: {}
    entity_id: media_player.kitchen_display
mode: single

Welcome to Home Assistant - hope you will enjoy it.

The Protect integration uses the stream component built-in to Home Assistant, and that gives a 10-20 seconds delay in the stream. I assume it is due to conversion of the RTSP stream.
If you don’t need the Audio, you can disable all the RTSP streams in the Protect APP, and then restart Home Assistant. This will improve the video delay, but you loose the audio.

Thanks! It seems that the same automation doesn’t work after turning off the streams:

Logger: homeassistant.components.stream.worker
Source: components/stream/worker.py:83
Integration: Stream (documentation, issues)
First occurred: 8:11:13 AM (4 occurrences)
Last logged: 8:12:47 AM

Error opening stream rtsp://192.168.1.1:7447/MyFWLhUVZ9qhSXId

It will work in Lovelace, displaying the stream, in a Picture Glance card. I can see it still uses the RTSP stream, so you might need to restart HA. I am not sure it will work though, as the camera.play_streammight require a real stream. I will play a little with it, to see if I can get it to work. Until then, enable the stream again in the Protect App, to at least get the stream with delay.

1 Like

hey @briis

I’m using this with a G4 doorbell. I don’t like how the stock app’s notification doesn’t send a thumbnail, by the time you get into the protect app, the person is gone.

So I successfully built an automation to push a notification to my mobile apps with a screenshot of the camera in HA. This notification has buttons for ‘view in HA’ and ‘view in Protect’.

For ‘view in protect’, I’m trying to grab the intent, so that I can take the user straight to the doorbell live view, just like the protect app does. So I grabbed a logcat and found this intent:

03-03 15:25:08.880  1335  4546 D OemSceneCallBlock: isCallBlockedWithUidIntent { flg=0x1000c000 cmp=com.ubnt.unifi.protect/com.ubnt.activities.DashboardActivity (has extras) }, ResolveInfo{307d3d2 com.ubnt.unifi.protect/com.ubnt.activities.DashboardActivity m=0x0}
03-03 15:25:08.881  1335  4546 I ActivityTaskManager: START u0 {flg=0x1000c000 cmp=com.ubnt.unifi.protect/com.ubnt.activities.DashboardActivity (has extras)} from uid 10692 pid -1
03-03 15:25:08.887  1335  4546 E ANDR-PERF-JNI: com_qualcomm_qtiperformance_native_perf_io_prefetch_start
03-03 15:25:08.894  1335  4546 D ActivityTrigger: ActivityTrigger activityPauseTrigger
03-03 15:25:08.896  1630  1632 E ANDR-IOP: io prefetch is disabled
03-03 15:25:08.897  3796  3796 D Launcher: onPause# hashcode: 165350502
03-03 15:25:08.897  3796  3796 I ShelfLauncherCallbacks: onPause
03-03 15:25:08.898  3796  3961 I WeatherProvider: un-subscribe the weather callback:
03-03 15:25:08.898  1335  1402 D OpPowerConsumpStatsInjector: notifyPkgEvent
03-03 15:25:08.898  1335  1402 D OpRestartProcessManager: updateSelf :  com.ubnt.unifi.protect, size : 30
03-03 15:25:08.900 12671 12671 W ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@2af5800
03-03 15:25:08.900  1335  4497 D OemSceneModeActivityStack: [scene] evaluateGameModes :  gameMsg.arg1=0 gameMsg.arg2=1
03-03 15:25:08.901  1335  4497 D OpQuickReply: setQuickReplyResumed focusedApp AppWindowToken{986f755 token=Token{53defa7 ActivityRecord{83f10a3 u0 com.ubnt.unifi.protect/com.ubnt.activities.DashboardActivity t23672}}} pkgName com.ubnt.unifi.protect

(I changed around some numbers in case there is identifying info here)

Is there any chance you know how to make sense of this? I have no clue how to sort out which part of this is the “intent”

cheers!

image

Hi @VinistoisR
Unfortunately I cannot help here. I think we need someone with mobile skills to help out.

I’m familiar with mobile, whatever @VinistoisR is trying to do is what I’d like to do as well.

How are you jumping directly to the specific camera view in HA? I can only figure out how to go to a “dashboard” in HA, not the actual live view. I think this is a limitation.

However, for jumping directly to Unifi Protect I think they have some protected adb intents. Jumping directly to a camera activity seems to result in a permission denial / security exception

./adb.exe shell am start -n com.ubnt.unifi.protect/com.ubnt.activities.timelapse.CameraActivity


Starting: Intent { cmp=com.ubnt.unifi.protect/com.ubnt.activities.timelapse.CameraActivity }

Exception occurred while executing 'start':
java.lang.SecurityException: Permission Denial: starting Intent { flg=0x10000000 cmp=com.ubnt.unifi.protect/com.ubnt.activities.timelapse.CameraActivity } from null (pid=15243, uid=2000) not exported from uid 10340
        at com.android.server.wm.ActivityStackSupervisor.checkStartAnyActivityPermission(ActivityStackSupervisor.java:1043)
        at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:999)
        at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:669)
        at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1096)
        at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1068)
        at com.android.server.am.ActivityManagerService.startActivityAsUserWithFeature(ActivityManagerService.java:3662)
        at com.android.server.am.ActivityManagerShellCommand.runStartActivity(ActivityManagerShellCommand.java:544)
        at com.android.server.am.ActivityManagerShellCommand.onCommand(ActivityManagerShellCommand.java:186)
        at android.os.BasicShellCommandHandler.exec(BasicShellCommandHandler.java:98)
        at android.os.ShellCommand.exec(ShellCommand.java:44)
        at com.android.server.am.ActivityManagerService.onShellCommand(ActivityManagerService.java:10505)
        at android.os.Binder.shellCommand(Binder.java:929)
        at android.os.Binder.onTransact(Binder.java:813)
        at android.app.IActivityManager$Stub.onTransact(IActivityManager.java:5053)
        at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:2867)
        at android.os.Binder.execTransactInternal(Binder.java:1159)
        at android.os.Binder.execTransact(Binder.java:1123)

However, when issuing

./adb.exe shell am start -n com.ubnt.unifi.protect/com.ubnt.sections.splash.SplashActivity

Starting: Intent { cmp=com.ubnt.unifi.protect/com.ubnt.sections.splash.SplashActivity }

This results in opening the Unifi Protect app’s last activity, which if the last thing you were on was the main activity, or a live camera view, it will be that.

Sending this as a HA notification would be the following, but it appears that you can lose the specific activity addition and just call on the app as the URI (https://companion.home-assistant.io/docs/notifications/actionable-notifications#android-example)

service: notify.mobile_app_pixel_3
data:
  message: 'test'
  data:
    actions:
      - action: "URI"
        title: "Open Unifi"
        uri: 'app://com.ubnt.unifi.protect'

According to the HA docs, actionable notifications only show two examples: URI / URL

However, it appears if you change the action using the following example, you can achieve specific intents to launch into apps with: https://companion.home-assistant.io/docs/notifications/notification-commands/#broadcast-intent

automation:
  - alias: Send broadcast intent
    trigger:
      ...
    action:
      service: notify.mobile_app_<your_device_id_here>
      data:
        message: "command_broadcast_intent"
        title: "action"
        data:
          channel: "com.ubnt.unifi.protect"

I’ve tried executing the above action FROM the notification shade via the android app, but I am afraid the android app doesn’t support this kind of response. If you wanted a specific intent, you’d probably have to send an action to your HA instance, which then would reply via an automation. However, I couldn’t get this to work as I think you need to be part of the correct “category” for the action, which the HA android app doesn’t allow doing via actions at the moment.

service: notify.mobile_app_pixel_3
data:
  message: command_broadcast_intent
  title: android.intent.action.MAIN
  data:
    channel: "com.ubnt.unifi.protect/com.ubnt.sections.splash.SplashActivity"

Spent a few hours looking at this for fun this morning. Hope this is helpful

1 Like

Hi @VinistoisR - can you post your automation for doing this? Sounds useful! Thanks!