Repository: briis/unifiprotect
Branch: master
Commit: 7ba7cdf52826
Files: 48
Total size: 310.2 KB
Directory structure:
gitextract_8gl2b350/
├── .devcontainer/
│ ├── Dockerfile
│ ├── automations.yaml
│ ├── configuration.yaml
│ └── devcontainer.json
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug-report.yml
│ │ └── feature-request.yml
│ └── workflows/
│ └── ci.yaml
├── .gitignore
├── .vscode/
│ ├── launch.json
│ └── tasks.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── blueprints/
│ └── automation/
│ └── unifiprotect/
│ ├── dynamic_doorbell.yaml
│ ├── push_notification_doorbell_event.yaml
│ ├── push_notification_motion_event.yaml
│ └── push_notification_smart_event.yaml
├── custom_components/
│ └── unifiprotect/
│ ├── __init__.py
│ ├── binary_sensor.py
│ ├── button.py
│ ├── camera.py
│ ├── config_flow.py
│ ├── const.py
│ ├── data.py
│ ├── entity.py
│ ├── light.py
│ ├── manifest.json
│ ├── media_player.py
│ ├── models.py
│ ├── number.py
│ ├── select.py
│ ├── sensor.py
│ ├── services.py
│ ├── services.yaml
│ ├── strings.json
│ ├── switch.py
│ ├── translations/
│ │ ├── da.json
│ │ ├── de.json
│ │ ├── en.json
│ │ ├── fr.json
│ │ ├── nb.json
│ │ └── nl.json
│ └── utils.py
├── hacs.json
├── info.md
├── pylintrc
├── pyproject.toml
└── unifiprotect.markdown
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/Dockerfile
================================================
FROM ghcr.io/ludeeus/devcontainer/integration:stable
RUN apt update \
&& sudo apt install -y libpcap-dev ffmpeg vim curl jq libturbojpeg0 \
&& mkdir -p /opt \
&& cd /opt \
&& git clone --depth=1 -b 2021.12.7 https://github.com/home-assistant/core.git hass \
&& python3 -m pip --disable-pip-version-check install --upgrade ./hass \
&& python3 -m pip install pyunifiprotect mypy black isort pyupgrade pylint pylint_strict_informational \
&& ln -s /workspaces/unifiprotect/custom_components/unifiprotect /opt/hass/homeassistant/components/unifiprotect
================================================
FILE: .devcontainer/automations.yaml
================================================
================================================
FILE: .devcontainer/configuration.yaml
================================================
default_config:
tts:
- platform: google_translate
stream:
ll_hls: true
part_duration: 0.75
segment_duration: 2
debugpy:
start: false
http:
server_port: 9123
logger:
default: warning
logs:
pyunifiprotect: debug
custom_components.unifiprotect: debug
automation: !include automations.yaml
================================================
FILE: .devcontainer/devcontainer.json
================================================
// See https://aka.ms/vscode-remote/devcontainer.json for format details.
{
"name": "HA unifiprotect",
"dockerFile": "Dockerfile",
"context": "..",
"appPort": [
"9123:9123"
],
"runArgs": [
"-v",
"${env:HOME}${env:USERPROFILE}/.ssh:/tmp/.ssh" // This is added so you can push from inside the container
],
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"ryanluker.vscode-coverage-gutters",
"ms-python.vscode-pylance",
"bungcip.better-toml",
],
"mounts": [
"type=volume,target=/config,src=vsc-dev-unifiprotect-ha-config,volume-driver=local"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"terminal.integrated.defaultProfile.linux": "bash",
"python.pythonPath": "/usr/local/python/bin/python",
"python.analysis.autoSearchPaths": false,
"python.formatting.blackArgs": [
"--line-length",
"88"
],
"python.formatting.provider": "black",
"python.linting.banditEnabled": false,
"python.linting.enabled": true,
"python.linting.flake8Enabled": false,
"python.linting.mypyEnabled": false,
"python.linting.pylintEnabled": true,
"python.linting.pylintArgs": [
"--rcfile=${workspaceFolder}/pyproject.toml"
],
"python.linting.pylamaEnabled": false,
"python.sortImports.args": [
"--settings-path=${workspaceFolder}/pyproject.toml"
],
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true,
"yaml.customTags": [
"!secret scalar"
]
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: Bug Report
description: File a bug report for the Home Assistant UniFi Protect Integration
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: ha-version
attributes:
label: Home Assistant Version?
description: What version of Home Assistant are you running?
placeholder: "2021.10.6"
validations:
required: true
- type: input
id: ufp-version
attributes:
label: UniFi Protect Version?
description: What version of UniFi Protect is installed?
placeholder: "1.20.0"
validations:
required: true
- type: input
id: integration-version
attributes:
label: UniFi Protect HA Integration version?
description: What version of Integration do you have installed in Home Assistant?
placeholder: "0.10.0"
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Describe the bug
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.yml
================================================
name: Feature Reuest
description: Ask for a new feature for the Home Assistant UniFi Protect Integration
labels: ["feature-request"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request.
- type: textarea
id: new-feature
attributes:
label: New Feature
description: Please describe in detail what feature you would like to have.
placeholder: Describe the feature
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
validations:
required: false
================================================
FILE: .github/workflows/ci.yaml
================================================
name: CI
on:
push:
branches:
- master
pull_request:
types: [opened, labeled, unlabeled, synchronize, ready_for_review, converted_to_draft]
schedule:
- cron: "0 0 * * *"
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v2"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Hassfest
uses: home-assistant/actions/hassfest@master
- name: Install Linting Dependencies
run: |
git clone --depth=1 -b dev https://github.com/home-assistant/core.git hass
rm -rf ./hass/homeassistant/components/unifiprotect
ln -s $GITHUB_WORKSPACE/custom_components/unifiprotect ./hass/homeassistant/components/unifiprotect
python -m pip install --upgrade pip
pip install pyunifiprotect mypy black isort pyupgrade pylint pylint_strict_informational
pip install ./hass
- name: isort
run: isort --check-only --quiet custom_components/unifiprotect
- name: black
run: black --check custom_components/unifiprotect
- name: mypy
run: cd ./hass && mypy homeassistant/components/unifiprotect
- name: pyupgrade
run: find . ! -path "./hass/*" -name "*.py" | xargs pyupgrade
- name: pylint
run: pylint --rcfile pyproject.toml custom_components/unifiprotect
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
unifiprotect.code-workspace
.DS_Store
.vscode/settings.json
met.no.json
custom_components/unifiprotect/test.py
.devcontainer/secrets.yaml
custom_components/unifiprotect/unifi_protect_server_new.py
custom_components/unifiprotect/unifi_protect_server_org.py
.devcontainer/secret.yaml
custom_components/unifiprotect/services-old.yaml
test.py
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
// Run "Start Home Assistant" task + `debugpy.start` HA service first
{
"name": "Python: Debug UniFi Protect",
"type": "python",
"request": "attach",
"justMyCode": false,
"port": 5678,
"host": "localhost",
"pathMappings": [
{
"localRoot": "/workspaces/unifiprotect",
"remoteRoot": "/config"
}
]
}
]
}
================================================
FILE: .vscode/tasks.json
================================================
{
"version": "2.0.0",
"tasks": [
{
"label": "Start Home Assistant",
"type": "shell",
"command": "container start",
"problemMatcher": []
}
{
"label": "Set Home Assistant Version",
"type": "shell",
"command": "container set-version",
"problemMatcher": []
},
{
"label": "Install Development Home Assistant Version",
"type": "shell",
"command": "container install",
"problemMatcher": []
}
{
"label": "Update Translations",
"type": "shell",
"command": "echo 'unifiprotect\n' | python3 -m script.translations develop",
"options": {
"cwd": "/opt/hass",
},
"problemMatcher": []
}
]
}
================================================
FILE: CHANGELOG.md
================================================
# // Changelog
## 0.12.0
0.12.0 was originally planned as a beta only release, but after giving it more thought, I figured it would be be great to mark it as stable for the folks that cannot upgrade to the HA core version in 2022.2.
This release is primarily fixes from the HA core process. There is also full support added for the G4 Doorbell Pro, the UP Sense.
This will be the **last** HACS release. After this point, the HACS repo is considered deprecated. We will still take issues in the repo as if people prefer to make them here instead of the HA core repo. But after a month or 2 we plan to archive the repo and have the integration removed from HACS.
### Differences between HACS version 0.12.0 and HA 2022.2.0b1 version:
#### HACS Only
* Migration code for updating from `0.10.x` or older still exists; this code has been _removed_ in the HA core version
#### HA Core Only
* Full language support. All of the languages HA core supports via Lokalise has been added to the ingration.
* Auto-discovery. If you have a Dream machine or a Cloud Key/UNVR on the same VLAN, the UniFi Protect integration will automatically be discovered and prompted for setup.
* UP Doorlock support. The HA core version has full support for the newly release EA UP Doorlock.
### Changes
* `CHANGE`: **BREAKING CHANGE** Removes all deprecations outlined in the 0.11.x release.
* `CHANGE`: **BREAKING CHANGE** The "Chime Duration" number entity has been replaced with a "Chime Type" select entity. This makes Home Assistant work the same way as UniFi Protect. (https://github.com/briis/unifiprotect/issues/451)
* `CHANGE`: **BREAKING CHANGE** Smart Sensor support has been overhauled and improved. If you have Smart Sensors, it is _highly recommended to delete your UniFi Protect integration config and re-add it_. Some of the categories for the sensors have changed and it is not easy to change those without re-adding the integration. The sensors for the Smart Sensor are may also appear unavaiable if that sensor is not configured to be abled. For example, if your have motion disabled on your Sensor in UniFi Protect, the motion sensor will be unavaiable in Home Assistnat. Full list of new Smart Sensor entites:
* Alarm Sound and Tampering binary sensors
* Motion Sensitivity number
* Mount Type and Paired Camera selects
* Status Light switch
* Configuration switches for various sensors:
* Motion Detection switch
* Temperature Sensor switch
* Humidity Sensor switch
* Light Sensor switch
* Alarm Sound Detection switch
* `CHANGE`: **BREAKING CHANGE** Removes `profile_ws` debug service. Core plans to add a more centralized way of getting debug information from an integration. This will be back in some form after that feature is added (estimate: 1-2 major core releases).
* `CHANGE`: **BREAKING CHANGE** Removes `event_thumbnail` attribute and associated `ThumbnailProxyView`. After a lot of discussion, core does not want to add more attributes with access tokens inside of attributes. We plan to add back event thumbnails in some form again. If you would like to follow along with the dicussion, checkout the [architecure dicussion for it](https://github.com/home-assistant/architecture/discussions/705).
* `CHANGE`: Switches Doorbell binary_sensor to use `is_ringing` attr, should great improve relaiability of the sensor
* `CHANGE`: Dynamic select options for Doorbell Text
* `CHANGE`: Improves names for a number of entities
* `CHANGE`: Adds a bunch of extra debug logging for entity updates
* `NEW`: Adds full support for the package camera for the G4 Doorbell Pro. It should now always be enabled by default (if you are upgrading from an older version, it will still be disabled). The snapshot for the Package Camera has also been fixed. Since the camera if only 2 FPS, _streaming is disabled_ to prevent buffering.
* `FIX`: Overhaul of the Websocket code. Websocket reconnects should be drastically improved. Hopefully all reconnnect issues should be gone now.
* `FIX`: Fixes NVR memory sensor if no data is reported
* `FIX`: Fixes spelling typo with Recording Capacity sensor (https://github.com/briis/unifiprotect/issues/440)
* `FIX`: Fixes `is_connected` check for cameras
* `FIX`: Adds back `last_trip_time` attribute to camera motion entity
* `FIX`: Fixes NVR memory sensor if no data is reported
* `FIX`: Fixes spelling typo with Recording Capacity sensor (https://github.com/briis/unifiprotect/issues/440)
* `FIX`: Further improves relibility of Doorbell binary_sensor
* `FIX`: Fixes voltage unit for doorbell voltage sensor
* `FIX`: Fixes `connection_host` for Cameras so it can have DNS hosts in addition to IPs.
* `FIX`: Improves relibility of entities when UniFi Protect goes offline and/or a device goes offline. Everything recovery seemlessly when UniFi Protect upgrades or firmware updates are applied (fixes https://github.com/briis/unifiprotect/issues/432).
* `FIX`: Improves relibility of `media_player` entities so they should report state better and be able to play longer audio clips.
* `FIX`: Fixes stopping in progress audio for `media_player` entities.
* `FIX`: Allows DNS hosts in addition to IP addresses (fixes https://github.com/briis/unifiprotect/issues/431).
* `FIX`: Fixes selection of default camera entity for when it is not the High Quality channel.
* `FIX`: Fixes https://github.com/briis/unifiprotect/issues/428. All string enums are now case insensitive.
* `FIX`: Fixes https://github.com/briis/unifiprotect/issues/427, affected cameras will automatically be converted to Detections recording mode.
## 0.12.0-beta10
This is the last planned release for the HACS version. This release primarily adds new features for the G4 Doorbell Pro and the Smart Sensor. This release does unfortunatly have a couple of breaking changes for people with doorbells and Smart Sensors which are avoidable due to how soon the Home Assistant core release is.
* `CHANGE`: **BREAKING CHANGE** The "Chime Duration" number entity has been replaced with a "Chime Type" select entity. This makes Home Assistant work the same way as UniFi Protect. (https://github.com/briis/unifiprotect/issues/451)
* `CHANGE`: **BREAKING CHANGE** Smart Sensor support has been overhauled and improved. If you have Smart Sensors, it is _highly recommended to delete your UniFi Protect integration config and re-add it_. Some of the categories for the sensors have changed and it is not easy to change those without re-adding the integration. The sensors for the Smart Sensor are may also appear unavaiable if that sensor is not configured to be abled. For example, if your have motion disabled on your Sensor in UniFi Protect, the motion sensor will be unavaiable in Home Assistnat. Full list of new Smart Sensor entites:
* Alarm Sound and Tampering binary sensors
* Motion Sensitivity number
* Mount Type and Paired Camera selects
* Status Light switch
* Configuration switches for various sensors:
* Motion Detection switch
* Temperature Sensor switch
* Humidity Sensor switch
* Light Sensor switch
* Alarm Sound Detection switch
* `CHANGE`: Adds full support for the package camera for the G4 Doorbell Pro. It should now always be enabled by default (if you are upgrading from an older version, it will still be disabled). The snapshot for the Package Camera has also been fixed. Since the camera if only 2 FPS, _streaming is disabled_ to prevent buffering.
* `FIX`: Overhaul of the Websocket code. Websocket reconnects should be drastically improved. Hopefully all reconnnect issues should be gone now.
## 0.12.0-beta9
Home Assistant core port complete! The version that is in `2022.2` will officially have all of the same features. This is the final backport version to make sure the two versions are equal. The only difference between `0.12.0-beta9` and the code in `2022.2` is
* Migration code from `< 0.11.x` has been dropped. You must be on at least `0.11.0` or newer to migrate to the Home Assistant core version.
Additionally, we could not add _every_ feature from the HACS version to the HA core version so there are 2 additional breaking changes in this release (sorry!):
* `CHANGE`: **BREAKING CHANGE** Removes `profile_ws` and `take_sample` debug services. Core plans to add a more centralized way of getting debug information from an integration. This will be back in some form after that feature is added (estimate: 1-2 major core releases).
* `CHANGE`: **BREAKING CHANGE** Removes `event_thumbnail` attribute and associated `ThumbnailProxyView`. After a lot of discussion, core does not want to add more attributes with access tokens inside of attributes. We plan to add back event thumbnails in some form again. If you would like to follow along with the dicussion, checkout the [architecure dicussion for it](https://github.com/home-assistant/architecture/discussions/705).
Going forward, there will be some new features for the 0.12.0-beta / core version that will be developed for core version and then be backported to the HACS version. These include improvements for the G4 Doorbell Pro and the UP Sense devices.
## 0.12.0-beta8
* `FIX`: Fixes NVR memory sensor if no data is reported
* `FIX`: Fixes spelling typo with Recording Capacity sensor (https://github.com/briis/unifiprotect/issues/440)
* `FIX`: Fixes `is_connected` check for cameras
* `FIX`: Adds back `last_trip_time` attribute to camera motion entity
## 0.12.0-beta7
* `FIX`: Fixes NVR memory sensor if no data is reported
* `FIX`: Fixes spelling typo with Recording Capacity sensor (https://github.com/briis/unifiprotect/issues/440)
* `FIX`: Fixes is_connected check for cameras
* `FIX`: Adds back `last_trip_time` attribute to camera motion entity
## 0.12.0-beta7
* `FIX`: Improve relibility of Websocket reconnects
* `FIX`: Further improves relibility of Doorbell binary_sensor
## 0.12.0-beta6
* `CHANGE`: Switches Doorbell binary_sensor to use `is_ringing` attr, should great improve relaiability of the sensor
* `NEW`: Adds `take_sample` service to help with debugging/issue reporting
* `FIX`: Fixes voltage unit for doorbell voltage sensor
Backports changes from Home Assistant core merge process:
* `CHANGE`: Dynamic select options for Doorbell Text
* `CHANGE`: Improves names for a number of entities
* `CHANGE`: Adds a bunch of extra debug logging for entity updates
## 0.12.0-beta5
* `FIX`: Fixes `connection_host` for Cameras so it can have DNS hosts in addition to IPs.
## 0.12.0-beta4
Backports fixes from Home Assistant core merge process:
* `FIX`: Improves relibility of entities when UniFi Protect goes offline and/or a device goes offline. Everything recovery seemlessly when UniFi Protect upgrades or firmware updates are applied (fixes https://github.com/briis/unifiprotect/issues/432).
* `FIX`: Improves relibility of `media_player` entities so they should report state better and be able to play longer audio clips.
* `FIX`: Fixes stopping in progress audio for `media_player` entities.
* `FIX`: Allows DNS hosts in addition to IP addresses (fixes https://github.com/briis/unifiprotect/issues/431).
* `FIX`: Fixes selection of default camera entity for when it is not the High Quality channel.
## 0.12.0-beta3
* `FIX`: Fixes https://github.com/briis/unifiprotect/issues/428. All string enums are now case insensitive.
## 0.12.0-beta2
* `FIX`: Fixes https://github.com/briis/unifiprotect/issues/427, affected cameras will automatically be converted to Detections recording mode.
## 0.12.0-beta1
The 0.12.0-beta is designed to be a "beta only" release. There will not be a stable release for it. It is designed to test the final changes needed to merge the unifiprotect into Home Assistant core.
* `CHANGE`: **BREAKING CHANGE** Removes all deprecations outlined in the 0.11.x release.
## 0.11.2
* `FIX`: Setting up camera entities will no longer error if a camera does not have a channel. Will now result in log and continue
* `FIX`: Unadopted entities are ignored (fixes #420)
* `FIX`: Event thumbnails now return instantly using newer endpoint from UniFi Protect. They appear to come back as a camera snapshot until after the events ends, but they should always return an image now.
## 0.11.1
### Deprecations
As an amended to the deprecations from 0.11.0, the `last_tripped_time` is _no longer_ deprecated as `last_changed` is not a full replacement (#411)
### Other changes
* `FIX`: Bumps version of `pyunifiprotect` to 1.3.4. This will fix talkback for all cameras that was not working as expected
## 0.11.0
### Deprecations
0.11 is last major release planned before we merge the `unifiprotect` integration into core. As a result, a number of features are being removed when we merged into core.
The following services will be removed in the next version:
* `unifiprotect.set_recording_mode` -- use the select introduced in 0.10 instead
* `unifiprotect.set_ir_mode` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_status_light` -- use the switch entity on the camera device instead
* `unifiprotect.set_hdr_mode` -- use the switch entity on the camera device instead
* `unifiprotect.set_highfps_video_mode` -- use the switch entity on the camera device instead
* `unifiprotect.set_doorbell_lcd_message` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_mic_volume` -- use the number entity introduced in 0.10 instead
* `unifiprotect.set_privacy_mode` -- use the switch entity introduced in 0.10 instead
* `unifiprotect.set_zoom_position` -- use the number entity introduced in 0.10 instead
* `unifiprotect.set_wdr_value` -- use the number entity introduced in 0.10 instead
* `unifiprotect.light_settings` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_viewport_view` -- use the select entity introduced in 0.10 instead
The following events will be removed in the next version:
* `unifiprotect_doorbell` -- use a State Changed event on "Doorbell" binary sensor on the device instead
* `unifiprotect_motion` -- use a State Changed event on the "Motion" binary sensor on the device instead
The following entities will be removed in the next version:
* The "Motion Recording" sensor for cameras (in favor of the "Recording Mode" select)
* The "Light Turn On" sensor for flood lights (in favor of the "Lighting" select)
All of following attributes should be duplicated data that can be gotten from other devices/entities and as such, they will be removed in the next version.
* `device_model` will be removed from all entities -- provided in the UI as part of the "Device Info"
* `last_tripped_time` will be removed from binary sensor entities -- use the `last_changed` value provided by the [HA state instead](https://www.home-assistant.io/docs/configuration/state_object/)
* `up_since` will be removed from camera and light entities -- now has its own sensor. The sensor is disabled by default so you will need to enable it if you want to use it.
* `enabled_at` will be removed from light entities -- now has its own sensor
* `camera_id` will be removed from camera entities -- no services need the camera ID anymore so it does not need to be exposed as an attribute. You can still get device IDs for testing/debugging from the Configuration URL in the "Device Info" section
* `chime_duration`, `is_dark`, `mic_sensitivity`, `privacy_mode`, `wdr_value`, and `zoom_position` will be removed from camera entities -- all of them have now have their own sensors
* `event_object` will be removed from the Motion binary sensor. Use the dedicated Detected Object sensor.
### Breaking Changes in this release
* `CHANGE`: **BREAKING CHANGE** The internal name of the Privacy Zone controlled by the "Privacy Mode" switch has been changed. Make sure you turn off all of your privacy mode switches before upgrading. If you do not, you will need to manually delete the old Privacy Zone from your UniFi Protect app.
* `CHANGE`: **BREAKING CHANGE** WDR `number` entity has been removed from Cameras that have HDR. This is inline with changes made to Protect as you can no longer control WDR for cameras with HDR.
* `CHANGE`: **BREAKING CHANGE** the `event_length` attribute has been removed from the motion and door binary sensors. The value was previously calculated in memory and not reliable between restarts.
* `CHANGE`: **BREAKING CHANGE** the `event_object` attribute for binary motion sensors has changed the value for no object detected from "None Identified" (string) to "None" (NoneType/null)
* `CHANGE`: **BREAKING CHANGE** The Doorbell Text select entity for Doorbells has been overhauled. The Config Flow option for Doorbell Messages has been removed. You now can use the the `unifiprotect.add_doorbell_text` and `unifiprotect.remove_doorbell_text` services to add/remove Doorbell messages. This will persist the messages in UniFi Protect and the choices will now be the same ones that appear in the UniFi Protect iOS/Android app. **NOTE**: After running one of these services, you must restart Home Assistant for the updated options to appear.
### Other Changes in this release
* `CHANGE`: Migrates `UpvServer` to new `ProtectApiClient` from `pyunifiprotect`.
* This should lead to a number of behind-the-scenes reliability improvements.
* Should fix/close the following issues: #248, #255, #297, #317, #341, and #360 (TODO: Verify)
* `CHANGE`: Overhaul Config Flow
* Adds Reauthentication support
* Adds "Verify SSL"
* Updates Setup / Reauth / Options flows to pre-populate forms from existing settings
* Removes changing username/password as part of the options flow as it is redundant with Reauthentication support
* Removes Doorbell Text option since it is handled directly by UniFi Protect now
* Adds new config option to update all metrics (storage stat usage, uptimes, CPU usage, etc.) in realtime. **WARNING**: Enabling this option will greatly increase your CPU usage. ~2x is what we were seeing in our testing. It is recommended to leave it disabled for now as we do not have a lot of diagnostic sensors using this data yet.
* `CHANGE`: The state of the camera entities now reflects on whether the camera is actually recording. If you set your Recording Mode to "Detections", your camera will switch back and forth between "Idle" and "Recording" based on if the camera is actually recording.
* Closes #337
* `CHANGE`: Configuration URLs for UFP devices will now take you directly to the device in the UFP Web UI.
* `CHANGE`: Default names for all entities have been updated from `entity_name device_name` to `device_name entity_name` to match how Home Assistant expects them in 2021.11+
* `CHANGE`: The Bluetooth strength sensor for the UP Sense is now disabled by default (will not effect anyone that already has the sensor).
* `NEW`: Adds `unifiprotect.set_doorbell_message` service. This is just like the `unifiprotect.set_doorbell_lcd_message`, but it is not deprecated and it requires the Doorbell Text Select entity instead of the Camera entity. Should **only** be used to set dynamic doorbell text messages (i.e. setting the current outdoor temperate on your doorbell). If you want to use static custom messages, use the Doorbell Text Select entity and the `unifiprotect.add_doorbell_text` / `unifiprotect.remove_doorbell_text` service. `unifiprotect.set_doorbell_lcd_message` is still deprecated and will still be removed in the next release.
* Closes #396
* `NEW`: Adds "Override Connection Host" config option. This will force your RTSP(S) connection IP address to be the same as everything else. Should only be used if you need to forcibly use a different IP address.
* For sure closes #248
* `NEW`: Added Dark Mode brand images to https://github.com/home-assistant/brands.
* `NEW`: Adds `phy_rate` and `wifi_signal` sensors so all connection states (BLE, WiFi and Wired) should have a diagnostic sensor. Disabled by default. Requires "Realtime metrics" option to update in realtime.
* `NEW`: Added Detected Object sensor for cameras with smart detections. Values are `none`, `person` or `vehicle`. Contains `event_score` and `event_thumb` attributes.
* Closes #342
* `NEW`: Adds Paired Camera select entity for Viewports
* `NEW`: Adds "Received Data", "Transferred Data", "Oldest Recording", "Storage Used", and "Disk Write Rate" sensors for cameras. Disabled by default. Requires "Realtime metrics" option to update in realtime.
* `NEW`: (requires UniFi Protect 1.20.1) Adds "Voltage" sensor for doorbells. Disabled by default.
* `NEW`: Adds "System Sounds" switch for cameras with speakers
* `NEW`: Adds switches to toggle overlay information for video feeds on all cameras
* `NEW`: Adds switches to toggle smart detection types on cameras with smart detections
* `NEW`: Adds event thumbnail proxy view.
* URL is `/api/ufp/thumbnail/{thumb_id}`. `thumb_id` is the ID of the thumbnail from UniFi Protect.
* `entity_id` is a required query parameters. `entity_id` be for an sensor that has event thumbnails on it (like the Motion binary sensor)
* `token` is a required query parameter is you are _not_ authenticated. It is an attribute on the motion sensor for the Camera
* `w` and `h` are optional query string params for thumbnail resizing.
* `NEW`: Adds `event_thumbnail` attribute to Motion binary sensor that uses above mentioned event thumbnail proxy view.
* `NEW`: Adds NVR sensors. All of them are disabled by default. All of the sensors will only update every ~15 minutes unless the "Realtime metrics" config option is turned on. List of all sensors:
* Disk Health (one per disk)
* System Info: CPU Temp, CPU, Memory and Storage Utilization
* Uptime
* Recording Capacity (in seconds)
* Distributions of stored video for Resolution (4K/HD/Free)
* Distributions of stored video for Type (Continuous/Detections/Timelapse)
* More clean up and improvements for upcoming Home Assistant core merge.
* Adds various new blueprints to help users automate UniFi Protect. New [Blueprints can be found in the README](https://github.com/briis/unifiprotect#automating-services)
## 0.11.0-beta.5
* `FIX`: Fixes motion events and sensors for UP-Sense devices (#405)
* `FIX`: Fixes error on start up for G4 Domes (#408)
## 0.11.0-beta.4
* `NEW`: Adds `unifiprotect.set_doorbell_message` service. This is just like the `unifiprotect.set_doorbell_lcd_message`, but it is not deprecated and it requires the Doorbell Text Select entity instead of the Camera entity. Should **only** be used to set dynamic doorbell text messages (i.e. setting the current outdoor temperate on your doorbell). If you want to use static custom messages, use the Doorbell Text Select entity and the `unifiprotect.add_doorbell_text` / `unifiprotect.remove_doorbell_text` service. `unifiprotect.set_doorbell_lcd_message` is still deprecated and will still be removed in the next release.
* Closes #396
* `NEW`: Adds "Override Connection Host" config option. This will force your RTSP(S) connection IP address to be the same as everything else. Should only be used if you need to forcibly use a different IP address.
* For sure closes #248
* `FIX`: Reset event_thumbnail attribute for Motion binary sensor after motion has ended
* `FIX`: Change unit for signal strength from db to dbm. (fixes Camera Wifi Signal Strength should be dBm not dB)
* Closes #394
* `NEW`: Added Dark Mode brand images to https://github.com/home-assistant/brands.
## 0.11.0-beta.3
* `DEPRECATION`: The Motion binary sensor will stop showing details about smart detections in the next version. Use the new separate Detected Object sensor. `event_object` attribute will be removed as well.
* `NEW`: Adds `phy_rate` and `wifi_signal` sensors so all connection states (BLE, WiFi and Wired) should have a diagnostic sensor. Disabled by default. Requires "Realtime metrics" option to update in realtime.
* `NEW`: Added Detected Object sensor for cameras with smart detections. Values are `none`, `person` or `vehicle`. Contains `event_score` and `event_thumb` attributes.
* Closes #342
* `NEW`: Adds Paired Camera select entity for Viewports
* `NEW`: Adds "Received Data", "Transferred Data", "Oldest Recording", "Storage Used", and "Disk Write Rate" sensors for cameras. Disabled by default. Requires "Realtime metrics" option to update in realtime.
* `NEW`: (requires UniFi Protect 1.20.1) Adds "Voltage" sensor for doorbells. Disabled by default.
* `NEW`: Adds "System Sounds" switch for cameras with speakers
* `NEW`: Adds switches to toggle overlay information for video feeds on all cameras
* `NEW`: Adds switches to toggle smart detection types on cameras with smart detections
## 0.11.0-beta.2
* `CHANGE`: Allows `device_id` parameter for global service calls to be any device from a UniFi Protect instance
* `NEW`: Adds event thumbnail proxy view.
* URL is `/api/ufp/thumbnail/{thumb_id}`. `thumb_id` is the ID of the thumbnail from UniFi Protect.
* `entity_id` is a required query parameters. `entity_id` be for an sensor that has event thumbnails on it (like the Motion binary sensor)
* `token` is a required query parameter is you are _not_ authenticated. It is an attribute on the motion sensor for the Camera
* `w` and `h` are optional query string params for thumbnail resizing.
* `NEW`: Adds `event_thumbnail` attribute to Motion binary sensor that uses above mentioned event thumbnail proxy view.
* `NEW`: Adds NVR sensors. All of them are disabled by default. All of the sensors will only update every ~15 minutes unless the "Realtime metrics" config option is turned on. List of all sensors:
* Disk Health (one per disk)
* System Info: CPU Temp, CPU, Memory and Storage Utilization
* Uptime
* Recording Capacity (in seconds)
* Distributions of stored video for Resolution (4K/HD/Free)
* Distributions of stored video for Type (Continuous/Detections/Timelapse)
* More clean up and improvements for upcoming Home Assistant core merge.
## 0.11.0-beta.1
### Deprecations
0.11 is last major release planned before we merge the `unifiprotect` integration into core. As a result, a number of features are being removed when we merged into core.
The following services will be removed in the next version:
* `unifiprotect.set_recording_mode` -- use the select introduced in 0.10 instead
* `unifiprotect.set_ir_mode` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_status_light` -- use the switch entity on the camera device instead
* `unifiprotect.set_hdr_mode` -- use the switch entity on the camera device instead
* `unifiprotect.set_highfps_video_mode` -- use the switch entity on the camera device instead
* `unifiprotect.set_doorbell_lcd_message` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_mic_volume` -- use the number entity introduced in 0.10 instead
* `unifiprotect.set_privacy_mode` -- use the switch entity introduced in 0.10 instead
* `unifiprotect.set_zoom_position` -- use the number entity introduced in 0.10 instead
* `unifiprotect.set_wdr_value` -- use the number entity introduced in 0.10 instead
* `unifiprotect.light_settings` -- use the select entity introduced in 0.10 instead
* `unifiprotect.set_viewport_view` -- use the select entity introduced in 0.10 instead
The following events will be removed in the next version:
* `unifiprotect_doorbell` -- use a State Changed event on "Doorbell" binary sensor on the device instead
* `unifiprotect_motion` -- use a State Changed event on the "Motion" binary sensor on the device instead
The following entities will be removed in the next version:
* The "Motion Recording" sensor for cameras (in favor of the "Recording Mode" select)
* The "Light Turn On" sensor for flood lights (in favor of the "Lighting" select)
All of following attributes should be duplicated data that can be gotten from other devices/entities and as such, they will be removed in the next version.
* `device_model` will be removed from all entities -- provided in the UI as part of the "Device Info"
* `last_tripped_time` will be removed from binary sensor entities -- use the `last_changed` value provided by the [HA state instead](https://www.home-assistant.io/docs/configuration/state_object/)
* `up_since` will be removed from camera and light entities -- now has its own sensor. The sensor is disabled by default so you will need to enable it if you want to use it.
* `enabled_at` will be removed from light entities -- now has its own sensor
* `camera_id` will be removed from camera entities -- no services need the camera ID anymore so it does not need to be exposed as an attribute. You can still get device IDs for testing/debugging from the Configuration URL in the "Device Info" section
* `chime_duration`, `is_dark`, `mic_sensitivity`, `privacy_mode`, `wdr_value`, and `zoom_position` will be removed from camera entities -- all of them have now have their own sensors
### Breaking Changes in this release
* `CHANGE`: **BREAKING CHANGE** The internal name of the Privacy Zone controlled by the "Privacy Mode" switch has been changed. Make sure you turn off all of your privacy mode switches before upgrading. If you do not, you will need to manually delete the old Privacy Zone from your UniFi Protect app.
* `CHANGE`: **BREAKING CHANGE** WDR `number` entity has been removed from Cameras that have HDR. This is inline with changes made to Protect as you can no longer control WDR for cameras with HDR.
* `CHANGE`: **BREAKING CHANGE** the `event_length` attribute has been removed from the motion and door binary sensors. The value was previously calculated in memory and not reliable between restarts.
* `CHANGE`: **BREAKING CHANGE** the `event_object` attribute for binary motion sensors has changed the value for no object detected from "None Identified" (string) to "None" (NoneType/null)
* `CHANGE`: **BREAKING CHANGE** The Doorbell Text select entity for Doorbells has been overhauled. The Config Flow option for Doorbell Messages has been removed. You now can use the the `unifiprotect.add_doorbell_text` and `unifiprotect.remove_doorbell_text` services to add/remove Doorbell messages. This will persist the messages in UniFi Protect and the choices will now be the same ones that appear in the UniFi Protect iOS/Android app. **NOTE**: After running one of these services, you must restart Home Assistant for the updated options to appear.
### Other Changes in this release
* `CHANGE`: Migrates `UpvServer` to new `ProtectApiClient` from `pyunifiprotect`.
* This should lead to a number of behind-the-scenes reliability improvements.
* Should fix/close the following issues: #248, #255, #297, #317, #341, and #360 (TODO: Verify)
* `CHANGE`: Overhaul Config Flow
* Adds Reauthentication support
* Adds "Verify SSL"
* Updates Setup / Reauth / Options flows to pre-populate forms from existing settings
* Removes changing username/password as part of the options flow as it is redundant with Reauthentication support
* Removes Doorbell Text option since it is handled directly by UniFi Protect now
* Adds new config option to update all metrics (storage stat usage, uptimes, CPU usage, etc.) in realtime. **WARNING**: Enabling this option will greatly increase your CPU usage. ~2x is what we were seeing in our testing. It is recommended to leave it disabled for now as we do not have a lot of diagnostic sensors using this data yet.
* `CHANGE`: The state of the camera entities now reflects on whether the camera is actually recording. If you set your Recording Mode to "Detections", your camera will switch back and forth between "Idle" and "Recording" based on if the camera is actually recording.
* Closes #337
* `CHANGE`: Configuration URLs for UFP devices will now take you directly to the device in the UFP Web UI.
* `CHANGE`: Default names for all entities have been updated from `entity_name device_name` to `device_name entity_name` to match how Home Assistant expects them in 2021.11+
* `CHANGE`: The Bluetooth strength sensor for the UP Sense is now disabled by default (will not effect anyone that already has the sensor).
* `NEW`: Adds all of the possible enabled UFP Camera channels as different camera entities; only the highest resolution secure (RTSPS) one is enabled by default. If you need RTSP camera entities, you can enable one of the given insecure camera entities.
* `NEW`: Added the following attributes to Camera entity: `width`, `height`, `fps`, `bitrate` and `channel_id`
* `NEW`: Added status light switch for Flood Light devices
* `NEW`: Added "On Motion - When Dark" option for Flood Light Lighting switch
* `NEW`: Added "Auto-Shutoff Timer" number entity for Flood Lights
* `NEW`: Added "Motion Sensitivity" number entity for Flood Lights
* `NEW`: Added "Chime Duration" number entity for Doorbells
* `NEW`: Added "Uptime" sensor entity for all UniFi Protect adoptable devices. This is disabled by default.
* `NEW`: Added `unifiprotect.set_default_doorbell_text` service to allow you to set your default Doorbell message text. **NOTE**: After running this service, you must restart Home Assistant for the default to be reflected in the options.
* `NEW`: Added "SSH Enabled" switch for all adoptable UniFi Protect devices. This switch is disabled by default.
* `NEW`: (requires 2021.12+) Added "Reboot Device" button for all adoptable UniFi Protect devices. This button is disabled by default. Use with caution as there is no confirm. "Pressing" it instantly reboots your device.
* `NEW`: Added media player entity for cameras with speaker. Speaker will accept any ffmpeg playable audio file URI (URI must be accessible from _Home Assistant_, not your Camera). TTS works great!
* TODO: Investigate for final release. This _may_ not work as expected on G4 Doorbells. Not sure yet if it is because of the recent Doorbell issues or because Doorbells are different.
* Implements #304
## 0.10.0
Released: 2021-11-24
> **YOU MUST BE RUNNING V1.20.0 OF UNIFI PROTECT, TO USE THIS VERSION OF THE INTEGRATION. IF YOU ARE STILL ON 1.19.x STAY ON THE 0.9.2 RELEASE.
As UniFi Protect V1.20.0 is now released, we will also ship the final release of 0.10.0. If you were not on the beta, please read these Release Notes carefully, as there are many changes for this release, and many Breaking Changes.
### Supported Versions
This release requires the following minimum Software and Firmware version:
* **Home Assistant**: `2021.09.0`
* **UniFi Protect**: `1.20.0`
### Upgrade Instructions
> If you are already running V0.10.0-beta.3 or higher of this release, there should not be any breaking changes, and you should be able to do a normal upgrade from HACS.
Due to the many changes and entities that have been removed and replaced, we recommend the following process to upgrade from an earlier Beta or from an earlier release:
* Upgrade the Integration files, either through HACS (Recommended) or by copying the files manually to your `custom_components/unifiprotect` directory.
* Restart Home Assistant
* Remove the UniFi Protect Integration by going to the Integrations page, click the 3 dots in the lower right corner of the UniFi Protect Integration and select *Delete*
* While still on this page, click the `+ ADD INTEGRATION` button in the lower right corner, search for UnFi Protect, and start the installation, supplying your credentials.
### Changes in this release
* `CHANGE`: **BREAKING CHANGE** The support for *Anonymous Snapshots* has been removed as of this release. This has always been a workaround in a time where this did not work as well as it does now. If you have this flag set, you don't have to do anything, as snapshots are automatically moved to the supported method.
* `NEW`: **BREAKING CHANGE** Also as part of Home Assistant 2021.11 a new [Entity Category](https://www.home-assistant.io/blog/2021/11/03/release-202111/#entity-categorization) is introduced. This makes it possible to classify an entity as either `config` or `diagnostic`. A `config` entity is used for entities that can change the configuration of a device and a `diagnostic` entity is used for devices that report status, but does not allow changes. These two entity categories have been applied to selected entities in this Integration. If you are not on HA 2021.11+ then this will not have any effect on your installation.
* `CHANGE`: **BREAKING CHANGE** There has been a substansial rewite of the underlying IO API Module (`pyunifiprotect`) over the last few month. The structure is now much better and makes it easier to maintain going forward. It will take too long to list all the changes, but one important change is that we have removed the support for Non UnifiOS devices. These are CloudKey+ devices with a FW lower than 2.0.24. I want to give a big thank you to @AngellusMortis and @bdraco for making this happen.
* `CHANGE`: **BREAKING CHANGE** As this release has removed the support for Non UnifiOS devices, we could also remove the Polling function for Events as this is served through Websockets. This also means that the Scan Interval is no longer present in the Configuration.
* `CHANGE`: **BREAKING CHANGE** To future proof the Select entities, we had to change the the way the Unique ID is populated. The entity names are not changing, but the Unique ID's are If you have installed a previous beta of V0.10.0 you will get a duplicate of all Select entities, and the ones that were there before, will be marked as unavailable. You can either remove them manually from the Integration page, or even easier, just delete the UniFi Protect integration, and add it again. (The later is the recommended method)
* `CHANGE`: **BREAKING CHANGE** All switches called `switch.ir_active_CAMERANAME` have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.set_ir_mode` now supports the following values for ir_mode: `"auto, autoFilterOnly, on, off"`. This is a change from the previous valid options and if you have automations that uses this service you will need to make sure that you only use these supported modes.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.save_thumbnail_image` has been removed from the Integration. This service proved to be unreliable as the Thumbnail image very often was not available, when this service was called. Please use the service `camera.snapshot` instead.
* `CHANGE`: **BREAKING CHANGE** All switches called `switch.record_smart_CAMERANAME` and `switch.record_motion_CAMERANAME` have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** All switches for the *Floodlight devices* have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.set_recording_mode` now only supports the following values for recording_mode: `"never, detections, always"`. If you have automations that uses the recording_mode `smart` or `motion` you will have to change this to `detections`.
* `CHANGE`: Config Flow has been slimmed down so it will only ask for the minimum values we need during installation. If you would like to change this after that, you can use the Configure button on the Integration page.
* `CHANGE`: It is now possible to change the UFP Device username and password without removing and reinstalling the Integration. On the Home Assistant Integration page, select CONFIGURE in the lower left corner of the UniFi Protect integration, and you will have the option to enter a new username and/or password.
* `CHANGE`: We will now use RTSPS for displaying video. This is to further enhance security, and to ensure that the system will continue running if Ubiquiti decides to remove RTSP completely. This does not require any changes from your side.
* `NEW`: For each Camera there will be a binary sensor called `binary_sensor.is_dark_CAMERANAME`. This sensor will be on if the camera is perceiving it is as so dark that the Infrared lights will turn on (If enabled).
* `CHANGE`: A significant number of 'under the hood' changes have been made by @bdraco, to bring the Integration up to Home Assistant standards and to prepare for the integration in to HA Core. Thank you to @bdraco for all his advise, coding and review.
* `CHANGE`: `pyunifiprotect` is V1.0.4 and has been completely rewritten by @AngellusMortis, with the support of @bdraco and is now a much more structured and easier to maintain module. There has also been a few interesting additions to the module, which you will see the fruit of in a coming release. This version is not utilizing the new module yet, but stay tuned for the 0.11.0 release, which most likely also will be the last release before we try the move to HA Core.
* `NEW`: Device Configuration URL's are introduced in Home Assistant 2021.11. In this release we add URL Link to allow the user to visit the device for configuration or diagnostics from the *Devices* page. If you are not on HA 2021.11+ then this will not have any effect on your installation.
* `NEW`: A switch is being created to turn on and off the Privacy Mode for each Camera. This makes it possible to set the Privacy mode for a Camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_privacy_mode`
* `NEW`: Restarted the work on implementing the UFP Sense device. We don't have physical access to this device, but @Madbeefer is kindly enough to do all the testing.
* The following new sensors will be created for each UFP Sense device: `Battery %`, `Ambient Light`, `Humidity`, `Temperature` and `BLE Signal Strength`.
* The following binary sensors will be created for each UFP Sense device: `Motion`, `Open/Close` and `Battery Low`. **Note** as of this release, these sensors are not working correctly, this is still work in progress.
* `NEW`: For each Camera there will now be a `Select Entity` from where you can select the Infrared mode for each Camera. Valid options are `Auto, Always Enable, Auto (Filter Only, no LED's), Always Disable`. These are the same options you can use if you set this through the UniFi Protect App.
* `NEW`: Added a new `Number` entity called `number.wide_dynamic_range_CAMERANAME`. You can now set the Wide Dynamic Range for a camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_wdr_value`.
* `NEW`: Added `select.doorbell_text_DOORBELL_NAME` to be able to change the LCD Text on the Doorbell from the UI. In the configuration menu of the Integration there is now a field where you can type a list of Custom Texts that can be displayed on the Doorbell and then these options plus the two standard texts built-in to the Doorbell can now all be selected. The format of the custom text list has to ba a comma separated list, f.ex.: RING THE BELL, WE ARE SLEEPING, GO AWAY... etc.
* `NEW`: Added a new `Number` entity called `number.microphone_level_CAMERANAME`. From here you can set the Microphone Sensitivity Level for a camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_mic_volume`.
* `NEW`: Added a new `Number` entity called `number.zoom_position_CAMERANAME`. From here you can set the optical Zoom Position for a camera directly from the UI. This entity will only be added for Cameras that support optical zoom. This is a supplement to the already existing service `unifiprotect.set_zoom_position`.
* `NEW`: For each Camera there will now be a `Select Entity` from where you can select the recording mode for each Camera. Valid options are `Always, Never, Detections`. Detections is what you use to enable motion detection. Whether they do People and Vehicle detection, depends on the Camera Type and the settings in the UniFi Protect App. We might later on implement a new Select Entity from where you can set the the Smart Detection options. Until then, this needs to be done from the UniFi Protect App. (as is the case today)
* `NEW`: For each Floodlight there will now be a `Select Entity` from where you can select when the Light Turns on. This replaces the two switches that were in the earlier releases. Valid options are `On Motion, When Dark, Manual`.
* `NEW`: Added a new event `unifiprotect_motion` that triggers on motion. You can use this instead of the Binary Sensor to watch for a motion event on any motion enabled device. The output from the event will look similar tom the below
```json
{
"event_type": "unifiprotect_motion",
"data": {
"entity_id": "camera.outdoor",
"smart_detect": [
"person"
],
"motion_on": true
},
"origin": "LOCAL",
"time_fired": "2021-10-18T10:55:36.134535+00:00",
"context": {
"id": "b3723102b4fb71a758a423d0f3a04ba6",
"parent_id": null,
"user_id": null
}
}
```
## 0.10.0 Beta 5 Hotfix 1
Released: November 13th, 2021
### Supported Versions
This release requires the following minimum Software and Firmware version:
* **Home Assistant**: `2021.09.0`
* **UniFi Protect**: `1.20.0-beta.7`
### Upgrade Instructions
Due to the many changes and entities that have been removed and replaced, we recommend the following process to upgrade from an earlier Beta or from an earlier release:
* Upgrade the Integration files, either through HACS (Recommended) or by copying the files manually to your `custom_components/unifiprotect` directory.
* Restart Home Assistant
* Remove the UniFi Protect Integration by going to the Integrations page, click the 3 dots in the lower right corner of the UniFi Protect Integration and select *Delete*
* While still on this page, click the `+ ADD INTEGRATION` button in the lower right corner, search for UnFi Protect, and start the installation, supplying your credentials.
### Changes in this release
* `CHANGE`: Updated `pyunifiprotect` to 1.0.2. Fixing errors that can occur when using Python 3.9 - Home Assistant uses that.
## 0.10.0 Beta 5
Released: November 13th, 2021
### Supported Versions
This release requires the following minimum Software and Firmware version:
* **Home Assistant**: `2021.09.0`
* **UniFi Protect**: `1.20.0-beta.7`
### Upgrade Instructions
Due to the many changes and entities that have been removed and replaced, we recommend the following process to upgrade from an earlier Beta or from an earlier release:
* Upgrade the Integration files, either through HACS (Recommended) or by copying the files manually to your `custom_components/unifiprotect` directory.
* Restart Home Assistant
* Remove the UniFi Protect Integration by going to the Integrations page, click the 3 dots in the lower right corner of the UniFi Protect Integration and select *Delete*
* While still on this page, click the `+ ADD INTEGRATION` button in the lower right corner, search for UnFi Protect, and start the installation, supplying your credentials.
### Changes in this release
As there were still some changes we wanted to do before releasing this, we decided to do one more Beta, before freezing.
* `CHANGE`: The support for *Anonymous Snapshots* has been removed as of this release. This had always been a workaround in a time where this did not work as well as it does now. If you have this flag set, you don't have to do anything, as snapshots are automatically moved to the supported method.
* `CHANGE`: Config Flow has been slimmed down so it will only ask for the minimum values we need during installation. If you would like to change this after that, you can use the Configure button on the Integration page.
* `CHANGE`: It is now possible to change the UFP Device username and password without removing and reinstalling the Integration. On the Home Assistant Integration page, select CONFIGURE in the lower left corner of the UniFi Protect integration, and you will have the option to enter a new username and/or password.
* `NEW`: For each Camera there will be a binary sensor called `binary_sensor.is_dark_CAMERANAME`. This sensor will be on if the camera is perceiving it is as so dark that the Infrared lights will turn on (If enabled).
* `CHANGE`: A significant number of 'under the hood' changes have been made, to bring the Integration up to Home Assistant standards and to prepare for the integration in to HA Core. Thank you to @bdraco for all his advise, coding and review.
* `CHANGE`: `pyunifiprotect` has been completely rewritten by @AngellusMortis, with the support of @bdraco and is now a much more structured and easier to maintain module. There has also been a few interesting additions to the module, which you will see the fruit of in a coming release. This version is not utilizing the new module yet, but stay tuned for the 0.11.0 release, which most likely also will be the last release before we try the move to HA Core.
## 0.10.0 Beta 4
Released: November 4th, 2021
**REMINDER** This version is only valid for **V1.20.0-beta.2** or higher of UniFi Protect. If you are not on that version, stick with V0.9.1.
### Upgrade Instructions
Due to the many changes and entities that have been removed and replaced, we recommend the following process to upgrade from an earlier Beta or from an earlier release:
* Upgrade the Integration files, either through HACS (Recommended) or by copying the files manually to your `custom_components/unifiprotect` directory.
* Restart Home Assistant
* Remove the UniFi Protect Integration by going to the Integrations page, click the 3 dots in the lower right corner of the UniFi Protect Integration and select *Delete*
* While still on this page, click the `+ ADD INTEGRATION` button in the lower right corner, search for UnFi Protect, and start the installation, supplying your credentials.
### Changes in this release
This will be the last beta with functional changes, so after this release it will only be bug fixes. The final release will come out when 1.20 of UniFi Protect is officially launched. Everything from Beta 1, 2 and 3 is included here, plus the following:
* `NEW`: Device Configuration URL's are introduced in Home Assistant 2021.11. In this release we add URL Link to allow the user to visit the device for configuration or diagnostics from the *Devices* page. If you are not on HA 2021.11+ then this will not have any effect on your installation.
* `NEW`: **BREAKING CHANGE** Also as part of Home Assistant 2021.11 a new [Entity Category](https://www.home-assistant.io/blog/2021/11/03/release-202111/#entity-categorization) is introduced. This makes it possible to classify an entity as either `config` or `diagnostic`. A `config` entity is used for entities that can change the configuration of a device and a `diagnostic` entity is used for devices that report status, but does not allow changes. These two entity categories have been applied to selected entities in this Integration. If you are not on HA 2021.11+ then this will not have any effect on your installation.
We would like to have feedback from people on this choice. Have we categorized too many entities, should we not use this at all. Please come with the feedback.
Entities which have the entity_category set:
* Are not included in a service call targetting a whole device or area.
* Are, by default, not exposed to Google Assistant or Alexa. If entities are already exposed, there will be no change.
* Are shown on a separate card on the device configuration page.
* Do not show up on the automatically generated Lovelace Dashboards.
* `NEW`: A switch is being created to turn on and off the Privacy Mode for each Camera. This makes it possible to set the Privacy mode for a Camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_privacy_mode`
* `NEW`: Restarted the work on implementing the UFP Sense device. We don't have physical access to this device, but @Madbeefer is kindly enough to do all the testing.
* The following new sensors will be created for each UFP Sense device: `Battery %`, `Ambient Light`, `Humidity`, `Temperature` and `BLE Signal Strength`.
* The following binary sensors will be created for each UFP Sense device: `Motion`, `Open/Close` and `Battery Low`. **Note** as of this beta, these sensors are not working correctly, this is still work in progress.
## 0.10.0 Beta 3
Released: October 27th, 2021
**REMINDER** This version is only valid for **V1.20.0-beta.2** or higher of UniFi Protect. If you are not on that version, stick with V0.9.1.
### Upgrade Instructions
Due to the many changes and entities that have been removed and replaced, we recommend the following process to upgrade from an earlier Beta or from an earlier release:
* Upgrade the Integration files, either through HACS (Recommended) or by copying the files manually to your `custom_components/unifiprotect` directory.
* Restart Home Assistant
* Remove the UniFi Protect Integration by going to the Integrations page, click the 3 dots in the lower right corner of the UniFi Protect Integration and select *Delete*
* While still on this page, click the `+ ADD INTEGRATION` button in the lower right corner, search for UnFi Protect, and start the installation, supplying your credentials.
### Changes in this release
Everything from Beta 1 and 2 is included here, plus the following:
* `CHANGE`: **BREAKING CHANGE** There has been a substansial rewite of the underlying IO API Module (`pyunifiprotect`) over the last few month. The structure is now much better and makes it easier to maintain going forward. It will take too long to list all the changes, but one important change is that we have removed the support for Non UnifiOS devices. These are CloudKey+ devices with a FW lower than 2.0.24. I want to give a big thank you to @AngellusMortis and @bdraco for making this happen.
* `CHANGE`: **BREAKING CHANGE** As this release has removed the support for Non UnifiOS devices, we could also remove the Polling function for Events as this is served through Websockets. This also means that the Scan Interval is no longer present in the Configuration.
* `CHANGE`: **BREAKING CHANGE** To future proof the Select entities, we had to change the the way the Unique ID is populated. The entity names are not changing, but the Unique ID's are If you have installed a previous beta of V0.10.0 you will get a duplicate of all Select entities, and the ones that were there before, will be marked as unavailable. You can either remove them manually from the Integration page, or even easier, just delete the UniFi Protect integration, and add it again. (The later is the recommended method)
* `CHANGE`: **BREAKING CHANGE** All switches called `switch.ir_active_CAMERANAME` have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.set_ir_mode` now supports the following values for ir_mode: `"auto, autoFilterOnly, on, off"`. This is a change from the previous valid options and if you have automations that uses this service you will need to make sure that you only use these supported modes.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.save_thumbnail_image` has been removed from the Integration. This service proved to be unreliable as the Thumbnail image very often was not available, when this service was called. Please use the service `camera.snapshot` instead.
* `NEW`: For each Camera there will now be a `Select Entity` from where you can select the Infrared mode for each Camera. Valid options are `Auto, Always Enable, Auto (Filter Only, no LED's), Always Disable`. These are the same options you can use if you set this through the UniFi Protect App.
* `NEW`: Added a new `Number` entity called `number.wide_dynamic_range_CAMERANAME`. You can now set the Wide Dynamic Range for a camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_wdr_value`.
* `NEW`: Added `select.doorbell_text_DOORBELL_NAME` to be able to change the LCD Text on the Doorbell from the UI. In the configuration menu of the Integration there is now a field where you can type a list of Custom Texts that can be displayed on the Doorbell and then these options plus the two standard texts built-in to the Doorbell can now all be selected. The format of the custom text list has to ba a comma separated list, f.ex.: RING THE BELL, WE ARE SLEEPING, GO AWAY... etc.
* `NEW`: Added a new `Number` entity called `number.microphone_level_CAMERANAME`. From here you can set the Microphone Sensitivity Level for a camera directly from the UI. This is a supplement to the already existing service `unifiprotect.set_mic_volume`.
* `NEW`: Added a new `Number` entity called `number.zoom_position_CAMERANAME`. From here you can set the optical Zoom Position for a camera directly from the UI. This entity will only be added for Cameras that support optical zoom. This is a supplement to the already existing service `unifiprotect.set_zoom_position`.
## 0.10.0 Beta 2
Released: October 24th, 2021
Everything from Beta 1 is included here, plus the following:
`CHANGE`: Changes to the underlying `pyunifiprotect` module done by @AngellusMortis to ensure all tests are passing and adding new functionality to be used in a later release.
`NEW`: Added a new event `unifiprotect_motion` that triggers on motion. You can use this instead of the Binary Sensor to watch for a motion event on any motion enabled device. The output from the event will look similar tom the below
```json
{
"event_type": "unifiprotect_motion",
"data": {
"entity_id": "camera.outdoor",
"smart_detect": [
"person"
],
"motion_on": true
},
"origin": "LOCAL",
"time_fired": "2021-10-18T10:55:36.134535+00:00",
"context": {
"id": "b3723102b4fb71a758a423d0f3a04ba6",
"parent_id": null,
"user_id": null
}
}
```
## 0.10.0 Beta 1
Released: October 17th, 2021
This is the first Beta release that will support **UniFi Protect 1.20.0**. There have been a few changes to the Protect API, that requires us to change this Integration. Unfortunately it cannot be avoided that these are Breaking Changes, so please read carefully below before you do the upgrade.
When reading the Release Notes for UniFi Protect 1.20.0-beta.2 the following changes are directly affecting this Integration:
* Integrate “Smart detections” and “Motion Detections” into “Detections”.
* Generate only RTSPS links for better security. (RTSP streams are still available by removing S from RTSPS and by changing port 7441 to 7447.
#### Changes implemented in this version:
* `CHANGE`: **IMPORTANT** You MUST have at least UniFi Protect **V1.20.0-beta.1** installed for this Integration to work. There are checks on both new installations and upgraded installations to see if your UniFi Protect App is at the right version number. Please consult the HA Logfile for more information if something does not work.
If you are not running the 1.20.0 beta, DO NOT UPGRADE. If you did anyway, you can just uninstall and select the 0.9.1 release from HACS and all should be running again.
* `CHANGE`: **BREAKING CHANGE** All switches called `switch.record_smart_CAMERANAME` and `switch.record_motion_CAMERANAME` have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** All switches for the *Floodlight devices* have been removed from the system. They are being migrated to a `Select Entity` which you can read more about below. If you have automations that turns these switches on and off, you will have to replace this with the `select.select_option` service, using the valid options described below for the `option` data.
* `CHANGE`: **BREAKING CHANGE** The Service `unifiprotect.set_recording_mode` now only supports the following values for recording_mode: `"never, detections, always"`. If you have automations that uses the recording_mode `smart` or `motion` you will have to change this to `detections`.
* `NEW`: For each Camera there will now be a `Select Entity` from where you can select the recording mode for each Camera. Valid options are `Always, Never, Detections`. Detections is what you use to enable motion detection. Whether they do People and Vehicle detection, depends on the Camera Type and the settings in the UniFi Protect App. We might later on implement a new Select Entity from where you can set the the Smart Detection options. Until then, this needs to be done from the UniFi Protect App. (as is the case today)
* `NEW`: For each Floodlight there will now be a `Select Entity` from where you can select when the Light Turns on. This replaces the two switches that were in the earlier releases. Valid options are `On Motion, When Dark, Manual`.
* `CHANGE`: We will now use RTSPS for displaying video. This is to further enhance security, and to ensure that the system will continue running if Ubiquiti decides to remove RTSP completely. This does not require any changes from your side.
## 0.9.1
Released: October 17th, 2021
This will be the final release for devices not running the UnifiOS. With the next official release, there will no longer be support for the CloudKey+ running a firmware lover than 2.0.
**NOTE** This release does not support UniFi Protect 1.20.0+. This will be supported in the next Beta release.
* `FIX`: Issue #297. Improves determining reason for bad responses.
## 0.9.0
Released: August 29th, 2021
* `NEW`: This release adds support for the UFP Viewport device. This is done by adding the `select` platform, from where the views defined in Unifi Protect can be selected. When changing the selection, the Viewport will change it's current view to the selected item. The `select` platform will only be setup if UFP Viewports are found in Unfi Protect. When you create a view in Unifi Protect, you must check the box *Shared with Others* in order to use the view in this integration.
**NOTE**: This new entity requires a minimum of Home Assistant 2021.7. If you are on an older version, the Integration will still work, but you will get an error during startup.
* `NEW`: As part of the support for the UFP Viewport, there also a new service being created, called `unifiprotect.set_viewport_view`. This service requires two parameters: The `entity_id` of the Viewport and the `view_id` of the View you want to set. `view_id` is a long string, but you can find the id number when looking at the Attributes for the `select` entity.
* `FIX`: Issue #264, missing image_width variable is fixed in this release.
* `CHANGE`: PR #276, Ensure setup is retried later when device is rebooting. Thanks to @bdraco
* `CHANGE`: PR #271. Updated README, to ensure proper capitalization. Thanks to @jonbloom
* `CHANGE`: PR #278. Allow requesting a custom snapshot width and height, to support 2021.9 release. Thank to @bdraco. Fixing Issue #282
## 0.9.0 Beta 2
Released: July 17th, 2021
* `BREAKING`: If you installed Beta 1, then you will have a media_player entity that is no longer used. You can disable it, or reinstall the Integration to get completely rid of it.
* `NEW`: This release adds support for the UFP Viewport device. This is done by adding the `select` platform, from where the views defined in Unifi Protect can be selected. When changing the selection, the Viewport will change it's current view to the selected item. The `select` platform will only be setup if UFP Viewports are found in Unfi Protect. When you create a view in Unifi Protect, you must check the box *Shared with Others* in order to use the view in this integration.
**NOTE**: This new entity requires a minimum of Home Assistant 2021.7
* `NEW`: As part of the support for the UFP Viewport, there also a new service being created, called `unifiprotect.set_viewport_view`. This service requires two parameters: The `entity_id` of the Viewport and the `view_id` of the View you want to set. `view_id` is a long string, but you can find the id number when looking at the Attributes for the `select` entity.
* `FIX`: Issue #264, missing image_width variable is fixed in this release.
## 0.9.0 Beta 1
Released: July 6th, 2021
* `NEW`: This release adds support for the UFP Viewport device. This is done by adding the `media_player` platform, from where the views defined in Unifi Protect can be selected as source. When selecting the source, the Viewport will change it's current view to the selected source. The `media_player` platform will only be setup if UFP Viewports are found in Unfi Protect.
* `NEW`: As part of the support for the UFP Viewport, there also a new service being created, called `unifiprotect.set_viewport_view`. This service requires two parameters: The `entity_id` of the Viewport and the `view_id` of the View you want to set. `view_id` is a long string, but you can find the id number when looking at the Attributes for the media_player.
## 0.8.9
Released: June 29th, 2021
* `FIXED`: During startup of the Integration, it would sometimes log `Error Code: 500 - Error Status: Internal Server Error`. (Issue #249) This was caused by some values not being available at startup.
* `CHANGE`: The service `unifiprotect.save_thumbnail_image` now creates the directories in the filename if they do not exist. Issue #250.
* `FIX`: We have started the integration of the new UFP-Sense devices, but these are not available in Europe yet, so the integration is not completed, and will not be, before I can get my hands on one of these devices. Some users with the devices, got a crash when running the latest version, which is now fixed. The integration is not completed, this fix, just removes the errors that were logged. Thanks to @michaeladam for finding this.
* `NEW`: When the doorbell is pressed, the integration now fires an event with the type `unifiprotect_doorbell`. You can use this in automations instead of monitoring the binary sensor. The event will look like below and only fire when the doorbell is pressed, so there will be no `false`event. If you have multiple doorbells you use the `entity_id` value in the `data` section to check which doorbell was pressed.
```json
{
"event_type": "unifiprotect_doorbell",
"data": {
"ring": true,
"entity_id": "binary_sensor.doorbell_kamera_doerklokke"
},
"origin": "LOCAL",
"time_fired": "2021-06-26T08:16:58.882088+00:00",
"context": {
"id": "6b8cbcecb61d75cbaa5035e2624a3051",
"parent_id": null,
"user_id": null
}
}
```
## 0.8.8
Released: May 22nd, 2021
* `NEW`: As stated a few times, there is a delay of 10-20 seconds in the Live Stream from UniFi Protect. There is not much this integration can do about it, but what we can do is, to disable the RTSP Stream, so that JPEG push is used instead. This gives an almost realtime experience, with the cost of NO AUDIO. As of this version you can disable the RTSP Stream from the Config menu.
* `FIXED`: Issue #235, where the aspect ratio of the Doorbell image was wrong when displayed in Lovelace or in Notifications. Now the aspect ratio is read from the camera, so all cameras should have a correct ratio.
## 0.8.7
Released: May 4th, 2021
* `CHANGED`: Added **iot_class** to `manifest.json` as per HA requirements
* `FIXED`: Ensure the event_object is not cleared too soon, when a smart detect event occurs. Issue #225. Thanks to @bdraco for the fix.
* `CHANGED`: Updated README.md with information on how to turn on Debug logger. Thank you @blaines
## 0.8.6
Released: April 25th, 2021
* `FIXED`: If authentication failed during setup or startup of the Integration it did not return the proper boolean, and did not close the session properly.
* `CHANGED`: Stop updates on stop event to prevent shutdown delay.
* `CHANGED`: Updated several files to ensure compatability with 2021.5+ of Home Assistant. Thanks to @bdraco for the fix.
## 0.8.5
Released: March 30th, 2021
* `ADDED`: Have you ever wanted to silence your doorbell chime when you go to bed, or you put your child to sleep? - Now this is possible. A new service to enable/disable the attached Doorbel Chime is delivered with this release. The service is called `unifiprotect.set_doorbell_chime_duration` and takes two parameters: Entity ID of the Doorbell, Duration in milliseconds which is a number between 0 and 10000. 0 equals no chime. 300 is the standard for mechanical chimes and 10000 is only used in combination with a digital chime. The function does not really exist in the API, so this is a workaround. Let me know what values are best for on with the different chimes. You might still hear a micro second of a ding, but nothing that should wake anyone up. Fixing issue #211
## 0.8.4
Released: March 18th, 2021
* `FIXED`: Issues when activating Services that required an Integer as value, and using a Template to supply that value. Services Schemas have now been converted to use `vol.Coerce(int)` instead of just `int`.
* `CHANGED`: All Services definitions have now been rewritten to use the new format introduced with the March 2021 Home Assistant release. **NOTE**: You might need to do a Hard Refresh of your browser to see the new Services UI.
* `FIXED`: When using the switches or service to change recording mode for a camera, the recording settings where reset to default values. This is now fixed, so the settings you do in the App are not modfied by activating the Service or Recording mode switches.
## 0.8.3
Released: March 3rd, 2021
* `ADDED`: New service `unifiprotect.set_wdr_value` which can set the Wide Dynamic Range of a camera to an integer between 0 and 4. Where 0 is disabled and 4 is full.
## 0.8.2
Released: February 4th, 2021
* `FIXED`: Use the UniFi Servers MAc address as unique ID to ensure that it never changes. Previously we used the name, and that can be changed by the user. This will help with stability and prevent integrations from suddenly stop working if the name of the UDMP, UNVR4 or CKP was changed.
* `FIXED`: Further enhance the fix applied in 0.8.1 to ensure the Integration loads even if the first update fails. Thanks to @bdraco for implementing this.
* `FIXED`: Sometimes we would be missing event_on or event_ring_on if the websocket connected before the integration setup the binary sensor. We now always return the full processed data, eliminating this error. Another fix by @bdraco
## 0.8.1
Released: January 28th, 2021
* `FIXED`: The service `unifiprotect.set_status_light` did not function, as it was renamed in the IO module. This has now been fixed so that both the service and the Switch work again.
* `FIXED`: Issue #181, Add a retry if the first update request fails on load of the Integration.
## 0.8.0
Released: January 8th, 2021
This release adds support for the new Ubiquiti Floodlight device. If found on your Protect Server, it will add a new entity type `light`, that will expose the Floodlight as a light entity and add support for turning on and off, plus adjustment of brightness.
There will also be support for the PIR motion sensor built-in to the Floodlight, and you will be able to adjust PIR settings and when to detect motion.
You must have UniFi Protect V1.17.0-beta.10+ installed for Floodlight Support. Below that version, you cannot add the Floodlight as a device to UniFi Protect.
THANK YOU again to @bdraco for helping with some of the code and for extensive code review. Without you, a lot of this would not have been possible.
* `ADDED`: New `light` entity for each Floodlight found. You can use the normal *light* services to turn on and off. Be aware that *brightness* in the Protect App only accepts a number from 1-6, so when you adjust brightness from Lovelace or the Service, the number here will be converted to a number between 1 and 6.
* `ADDED`: A Motion Sensor is created for each Floodlight attached. It will trigger motion despite the state of the Light. It will however not re-trigger until the time set in the *Auto Shutoff Timer* has passed.
* `ADDED`: New service `unifiprotect.light_settings`. Please see the README file for details on this Service.
* `FIXED`: Missing " in the Services description, prevented message to be displayed to the user. Thank you to @MarcJenningsUK for spotting and fixing this.
* `CHANGED`: Bumped `pyunifiprotect` to 0.28.8
**IMPORTANT**: With the official FW 2.0.24 for the CloudKey+ all UniFi Protect Servers are now migrated to UniFiOS. So as of this release, there will be no more development on the Non UniFiOS features. What is there will still be working, but new features will only be tested on UniFiOS. We only have access to very limited HW to test on, so it is not possible to maintain HW for backwards compatability testing.
#### This release is tested on:
*Tested* means that either new features work on the below versions or they don't introduce breaking changes.
* CloudKey+ G2: FW Version 2.0.24 with Unifi Protect V1.17.0-beta.13
* UDMP: FW Version 1.18.5 with Unifi Protect V1.17.0-beta.13
## Release 0.7.1
Released: January 3rd, 2021
* `ADDED`: New service `unifiprotect.set_zoom_position` to set the optical zoom level of a Camera. This only works on Cameras that support optical zoom.
The services takes two parameters: **entity_id** of the camera, **position** which can be between 0 and 100 where 0 is no zoom and 100 is maximum zoom.
A new attribue called `zoom_position` is added to each camera, showing the current zoom position. For cameras that does not support setting optical zoom, this will always be 0.
#### This release is tested on:
*Tested* means that either new features work on the below versions or they don't introduce breaking changes.
* CloudKey+ G2: FW Version 2.0.24 with Unifi Protect V1.16.9
* UDMP: FW Version 1.18.5 with Unifi Protect V1.17.0-beta.10
## Release 0.7.0
Released: December 20th, 2020
* `ADDED`: New service `unifiprotect.set_privacy_mode` to enable or disable a Privacy Zone, that blacks-out the camera. The effect is that you cannot view anything on screen. If recording is enabled, the camera will still record, but the only thing you will get is a black screen. You can enable/disable the microphone and set recording mode from this service, by specifying the values you see below.
If the camera already has one or more Privacy Zones set up, they will not be overwritten, and will still be there when you turn of this.
Use this instead of physically turning the camera off or on.
The services takes four parameters: **entity_id** of the camera, **privacy_mode** which can be true or false, **mic_level** which can be between 0 and 100 and **recording_mode** which can be never, motion, always or smart.
Also a new attribute called `privacy_mode` is added to each camera, that shows if this mode is enabled or not. (Issue #159)
* `CHANGED`: Some users are getting a warning that *verify_sll* is deprecated and should be replaced with *ssl*. We changed the pyunifiportect module to use `ssl` instead of `verify_sll` (Issue #160)
* `ADDED`: Dutch translation to Config Flow is now added. Thank you to @copperek for doing it.
* `FIXED`: KeyError: 'server_id' during startup of Unifi Protect. This error poped up occasionally during startup of Home Assistant. Thank you to @bdraco for fixing this. (Issue #147)
* `FIXED`: From V1.17.x of UniFi Protect, Non Adopted Cameras would be created as camera.none and creating all kinds of errors. Now these cameras will be ignored, until they are properly adopted by the NVR. Thank you to @bdraco for helping fixing this.
#### This release is tested on:
*Tested* means that either new features work on the below versions or they don't introduce breaking changes.
* CloudKey+ G2: FW Version 1.1.13 with Unifi Protect V1.13.37
* UDMP: FW Version 1.18.4-3 with Unifi Protect V1.17.0-beta.6
## Release 0.6.7
Released: December 15th, 2020
`ADDED`: New attribute on each camera called `is_dark`. This attribute is true if the camera sees the surroundings as dark. If infrared mode is set to *auto*, then infrared mode would be turned on when this changes to true.
`ADDED`: New Service `unifiprotect.set_mic_volume` to set the Sensitivity of the built-in Microphone on each Camera. Requires two parameters: *Camera Entity* and *level*, where level is a number between 0 and 100. If level is set to 0, the Camera will not react on Audio Events.
On each camera there is also now a new attribute called `mic_sensitivity` which displayes the current value.
See [README.md](https://github.com/briis/unifiprotect#create-input-slider-for-microphone-sensitivity) for instructions on how to setup an Input Slider in Lovelace to adjust the value.
`CHANGED`: Updated the README.md documentation and added more information and a TOC.
#### This release is tested on:
*Tested* means that either new features work on the below versions or they don't introduce breaking changes.
* CloudKey+ G2: FW Version 1.1.13 with Unifi Protect V1.13.37
* UDMP: FW Version 1.18.3 with Unifi Protect V1.17.0-beta.6
## Release 0.6.6
With the release of Unifi Protect V1.7.0-Beta 1, there is now the option of detecting Vehicles on top of the Person detection that is allready there. This is what Ubiquiti calls *Smart Detection*. Also you can now set recording mode to only look for Smart Detection events, meaning that motion is only activated if a person or a vehicle is detected on the cameras. Smart Detection requires a G4-Series camera and a UnifiOS device.
**NOTE**: If you are not running Unifi Protect V1.17.x then the new features introduced here will not apply to your system. It has been tested on older versions of Unifi Protect, and should not break any existing installations.
* **New** For all G4 Cameras, a new Switch will be created called *Record Smart*, where you can activate or de-active Smart Recording on the camera
* **New** The service `unifiprotect.set_recording_mode` now has a new option for `recording_mode` called *smart*. This will turn on Smart Recording for the selected Camera. Please note this will only work on G4-Series cameras.
* **Fix** When the G4 Doorbell disconnected or restarted, the Ring Sensor was triggered. This fix now ensures that this does not happen.
### This release is tested on:
*Tested* means that either new features work on the below versions or they don't introduce breaking changes.
* CloudKey+ G2: FW Version 1.1.13 with Unifi Protect V1.13.37
* UDMP: FW Version 1.18.3-5 with Unifi Protect V1.17.0-beta.1
* UNVR: FW Version 1.3.15 with Unifi Protect V1.15.0
## Release 0.6.5
* **Hotfix** The recording of motion score and motion length got out of sync with the motion detections on/off state. With this fix, motion score and length are now updated together with the off state of the binary motion sensors. This was only an issue for Non UnifiOS devices (*CloudKey+ users with the latest original firmware version or below*).
*This release is tested on*:
* CloudKey+ G2: FW Version 1.1.13 with Unifi Protect V1.13.37
* UDMP: FW Version 1.18.3-5 with Unifi Protect V1.16.8
## Release 0.6.4
* **Hotfix** for those who experience that motion sensors no longer work after upgrading to 0.6.3. Users affected will be those who are running a version of Unifi Protect that does not support SmartDetection.
*This release is tested on*:
* CloudKey+ G2: FW Version 1.1.13 with Unifi Protect V1.13.37
* UDMP: FW Version 1.18.3-4 with Unifi Protect V1.16.7
* UDMP FW Version 1.18.0 with Unifi Protect V1.14.11
## Release 0.6.3
@bdraco made some serious changes to the underlying IO module, that gives the following new features:
* When running UnifiOS on the Ubiquiti Device, events are now fully constructed from Websockets.
* Motion Events are now triggered regardless of the Recording Mode, meaning you can use your cameras as Motion Detectors. **Object detection** still requires that the Cameras recording mode is enabled (Motion or Always) as this information is only passed back when either of these are on.
**BREAKING** If your Automations trigger on Motion Detection from a Camera, and you assume that Recording is enabled on a camera then you now need to make a check for that in the Condition Section of your automation.
* Bumped pyunifiprotect to 0.24.3
## Release 0.6.2
* Changed text for Config Flow, to differ between UnifiOS and NON UNifiOS devices, instead of CloudKey and UDMP. This makes more sense, now that CloudKey+ also can run UnifiOS.
* Changed the default port in Config Flow, from 7443 to 443, as this will be the most used port with the update to CloudKey+
* Added a Debug option to Config Flow, so that we can capture the actual error message when trying to Authenticate.
## Release 0.6.1
@bdraco strikes again and fixed the following problems:
* If the system is loaded, we miss events because the time has already passed.
* If the doorbell is rung at the same time as motion, we don't see the ring event because the motion event obscures it.
* If the hass clock and unifi clock are out of sync, we see the wrong events. (Still recommend to ensure that unifi and hass clocks are synchronized.)
* The Doorbell is now correctly mapped as DEVICE_CLASS_OCCUPANCY.
## Release 0.6.0
The Integration has now been rewritten to use **Websockets** for updating events, giving a lot of benefits:
* Motion and doorbell updates should now happen right away
* Reduces the amount of entity updates since we now only update cameras that change when we poll instead of them all.
* Reduce the overall load on Home Assistant.
Unfortunately, Websockets are **only available for UnifiOS** powered devices (UDMP & UNVR), so this will not apply to people running on the CloudKey. Here we will still need to do polling. Hopefully Ubiquity, will soon move the CloudKey to UnifiOS or add Websockets to this device also.
All Credits for this rewrite goes to:
* @bdraco, who did the rewrite of both the IO module and the Integration
* @adrum for the initial work on the Websocket support
* @hjdhjd for reverse engineering the Websocket API and writing up the description
This could not have been done without all your work.
### Other changes
* When setting the LCD text on the Doorbell, this is now truncated to 30 Characters, as this is the maximum supported Characters. Thanks to @hjdhjd for documenting this.
* Fixed an error were sometimes the External IP of the Server was used for the Internal Stream. Thanks to @adrum for fixing this.
* Added Switch for changing HDR mode from Lovelace (Issue #128). This switch will only be created for Cameras that support HDR mode.
* Added Switch for changing High FPS mode from Lovelace (Issue #128). This switch will only be created for Cameras that support High FPS mode.
* Improved error handling.
* Added German translation for Config Flow. Thank you @SeraphimSerapis
## Release 0.5.8
Object Detection was introduced with 1.14 of Unifi Protect for the UDMP/UNVR with the G4 series of Cameras. (I am unsure about the CloudKey+, but this release should not break on the CloudKey+ even without object detection). This release now adds a new Attribute to the Binary Motion Sensors that will display the object detected. I have currently only seen `person` being detected, but I am happy to hear if anyone finds other objects. See below on how this could be used.
This release also introduces a few new Services, as per user request. Please note that HDR and High FPS Services will require a version of Unifi Protect greater than 1.13.x. You will still be able to upgrade, but the functions might not work.
* **New feature**: Turn HDR mode on or off, asked for in Issue #119. Only selected Cameras support HDR mode, but for those cameras that support it, you can now switch this on or off by calling the service: `unifiprotect.set_hdr_mode`. Please note that when you use this Service the stream will reset, so expect a drop out in the stream for a little while.
* **New feature**: Turn High FPS video mode on or off. The G4 Cameras support High FPS video mode. With this release there is now a service to turn this on or off. Call the service `unifiprotect.set_highfps_video_mode`.
* **New feature**: Set the LCD Message on the G4 Doorbell. There is now a new service called `unifiprotect.set_doorbell_lcd_message` from where you can set a Custom Text for the LCD. Closing Issue #104
* **New attribute** `event_object` that will add the object detected when Motion occurs. It will contain the string `None Identified` if no specific object is detected. If a human is detected it will return `person` in this attribute, which you can test for in an automation. (See README.md for an example)
## Release 0.5.6
New feature: Turn the Status Light on or off, asked for in Issue #102. With this release there is now the possibility to turn the Status light on each camera On or Off. This can be done in two ways:
1. Use the service `unifiprotect.set_status_light`
2. Use the new switch that will be created for each camera.
Disabled the Websocket update, that was introduced in 0.5.5, as it is currently not being used, and caused error messages when HA was closing down, due to not being stopped.
## Release 0.5.5
The latest beta of Unifi Protect includes the start of Ubiquiti's version of AI, and introduces a concept called Smart Detect, which currently can identify People on specific Camera models. When this is enabled on a Camera, the event type changes from a *motion* event to a *smartdetect* event, and as such these cameras will no longer trigger motion events.
This release is a quick fix for the people who have upgraded to the latest Unifi Protect Beta version. I will later introduce more Integration features based on these new Unifi Protect features.
## Release 0.5.4
A more permanent fix for Issue #88, where the snapshot images did not always get the current image. The API call has now been modified, so that it forces a refresh of the image when pulling it from the camera. Thank you to @rajeevan for finding the solution.
If you installed release 0.5.3 AND enabled *Anonymous Snapshots* you can now deselect that option again, and you will not have to enable the Anonymous Snapshot on each Camera.
## Release 0.5.3
Fix for Issue #88 - The function for saving a Camera Snapshot works fine for most people, but it turns out that the image it saves is only refreshed every 10-15 seconds. There might be a way to force a new image, but as the Protect API is not documented I have not found this yet. If you need the guaranteed latest image from the Camera, there is a way around it, and that is to enable Anonymous Snapshots on each Camera, as this function always gets the latest image directly from the Camera.
This version introduces a new option where you can enable or disable anonymous snapshots in the Unifi Integration. If enabled, it will use a different function than if disabled, but it will only work if you login to each of your Cameras and enable the *Anonymous Snapshot*.
To use the Anonymous Snapshot, after this update has been installed, do the following:
1. Login to each of your Cameras by going to http://CAMERA_IP. The Username is *ubnt* and the Camera Password can be found in Unifi Protect under *Settings*.
2. If you have never logged in to the Camera before, it might take you through a Setup procedure - just make sure to keep it in *Unifi Video* mode, so that it is managed by Unifi Protect.
3. Once you are logged in, you will see an option on the Front page for enabling Anonymous Snapshots. Make sure this is checked, and then press the *Save Changes* button.
4. Repeat step 3 for each of your Cameras.
5. Now go to the Integrations page in Home Assistant. Find the Unifi Protect Widget and press options.
6. Select the checkbox *Use Anonymous Snapshots* and press *Submit*
Now the Unfi Protect Integration will use the direct Snapshot from the Camera, without going through Unfi Protect first.
## Release 0.5.2
* Added exception handling when the http connection is dropped on a persistent connection. The Integration will now throw a `ConfigEntryNotReady` instead and retry.
## Release 0.5.1
Two fixes are implemented in this release:
1. Basic data for Cameras were pulled at the same interval as the Events for Motion and Doorbell. This caused an unnecessary load on the CPU. Now the base Camera data is only pulled every 60 seconds, to minimize that load.
2. A user reported that when having more than 20 Cameras attached, the Binary Sensors stayed in an unavailable state. This was caused by the fact that a poll interval of 2 seconds for the events, was not enough to go through all cameras, so the state was never reported back to Home Assistant. With this release there is now an option on the *Integration Widget* to change the Scan Interval to a value between 2 and 30 seconds. You **ONLY** have to make this adjustment if you experience that the sensors stay unavailable - so typically if you have many Cameras attached. Default is still 2 seconds.
3. The same user mentioned above, is running Unifi Protect on the new NVR4 device, and the Integration seems to work fine on this new platform. I have not heard from anyone else on this, but at least one user has success with that.
4. Bumped pyunifiprotect to v0.16 which fixes the problem mentioned in point 1 above. Thank you to @bdraco for the fix.
## Release 0.5 - Fully Integration based
The Beta release 0.4 has been out for a while now, and I belive we are at a stage where I will release it officially, and it will be called Version 0.5.
After the conversion to use all the Async Libraries, it became obvious to also move away from *Yaml Configuration*, to the fully UI based Integration. As I wrote in the Tester Notes, I know there are some people with strong feelings about this, but I made the decision to make the move, and going forward **only** to support this way of adding Unifi Protect to Home Assistant.
### ***** BREAKING CHANGES *****
Once setup, the base functionality will be the same as before, with the addition of a few minor changes. But behind the scene there are many changes in all modules, which also makes this a lot more ready for becoming an official Integration in Home Assistant Core.
I want to send a BIG THANK YOU to @bdraco who has made a lot of code review, and helped me shape this to conform to Home Assistant standards. I learned so much from your suggestions and advice, so thank you!
Here are the Breaking changes:
1. Configuration can only be done from the *Integration* menu on the *Configuration* tab. So you will have to remove all references to *unifiprotect* from your configuration files.
2. All entities will get the `unifiprotect_` prefix removed from them, so you will have to change automations and scripts where you use these entities. This is done to make sure that entities have a Unique Id and as such can be renamed from the UI as required by Home Assistant. I will give a 99% guarantee, that we do not need to change entity names again.
### Upgrading and Installing
If you have not used Unifi Protect before, go to step 4.
If you are already runing a version of *Unifi Protect* with version 0.3.x or lower:
1. Remove the previous installation
* If you have installed through HACS, then go to HACS and remove the Custom Component
* If you manually copied the files to your system, go to the `custom_components` directory and delete the `unifiprotect` directory.
* Edit `configuration.yaml` and remove all references to *unifiprotect*. Some have split the setup in to multiple files, so remember to remove references to unifiprotect from these files also.
* I recommend to restart Home Assistant at this point, but in theory it should not be necessary.
4. Install the new version
* If you use HACS, go there, and add Unifi Protect V0.5 or later.
* If you do it manually, go to [Github](https://github.com/briis/unifiprotect/tree/master/custom_components/unifiprotect) and copy the files to `custom_components/unifiprotect`. Remember to include the `translations` directory and the files in here.
* Restart Home Assistant
* Now go to the *Integration* menu on the *Configuration* tab, and search for *Unifi Protect*. If it does not show up, try and clear your browser cache, and refresh your browser.
* From there, it should be self explanatory.
I will leave Release 0.3.4 as an option in HACS, so if you want to stick with the Yaml version, feel free to do so, but please note that I will not make any changes to this version going forward.
## Version 0.3.2
**NOTE** When upgrading Home Assistant to +0.110 you will receive warnings during startup about deprecated BinaryDevice and SwitchDevice. There has been a change to SwitchEntity and BinaryEntity in HA 0.110. For now this will not be changed, as not everybody is on 0.110 and if changing it this Component will break for users not on that version as it is not backwards compatible.
With this release the following items are new or have been fixed:
* **BREAKING** Attribute `last_motion` has been replaced with `last_tripped_time` and attribute `motion_score` has been replaced with `event_score`. So if you use any of these attributes in an automation you will need to change the automation.
* **NEW** There is now support for the Unifi Doorbell. If a Doorbell is discovered there will be an extra Binary Sensor created for each Doorbell, so a Doorbell Device will have both a Motion Binary Sensor and a Ring Binary Sensor. The later, turns True if the doorbell is pressed.
**BREAKING** As part of this implementation, it is no longer possible to define which binary sensors to load - all motion and doorbell *binary sensors* found are loaded. So the `monitored_condition` parameter is removed from the configuration for `binary_sensor` and needs to be removed from your `configuration.yaml` file if present.
* **FIX** The Switch Integration was missing a Unique_ID
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Bjarne Riis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# // UniFi Protect for Home Assistant
## THIS REPOSITORY IS NOW ARCHIEVED AND READONLY.
The `unifiprotect` integration is now in Home Assistant core, so no more updates will be made to this repository. Please visit the official documentation for [UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect/) to read more.
-----
## ⚠️ ⚠️ WARNING ABOUT Home Assistant v2022.2
The `unifiprotect` integration will be in Home Assistant core v2022.2. If you are running **0.10.x or older** of the HACS integration, **do not install v2022.2.x of Home Assistant core**.
If you are running 0.11.x or the 0.12.0, you should be safe to delete the HACS version as part of your upgrade. The 0.11.x branch is designed to be compatible with the 0.12.0-beta and the HA core version. The latest version of 0.12.0-beta will be the version of `unifiprotect` in HA core in v2022.0.
This repo is now **deprecated** in favor of the Home Assistant core version. This repo will be archived and removed from HACS after the 2022.4 release of Home Assistant.
### Reporting Issues
We have disable reporting issues to the HACS Github repo for the `unifiprotect` integration. If you have an issue you would like to report for the `unifiprotect` integration, please make you are running the HA core version of the integration provided by 2022.2.0 or new and then report your issue on the [HA core repo](https://github.com/home-assistant/core/issues/new/choose).
If you would still like to discuss the HACS version of the `unifiprotect` integration, feel free to use the [dicussions section](https://github.com/briis/unifiprotect/discussions) or the [HA Community forums thread](https://community.home-assistant.io/t/custom-component-unifi-protect/158041/865).
### Migration to HA Core Version Steps
If you have Smart Sensor devices and you are **not** running `0.12.0-beta10` or newer, it is recommended you just delete your UniFi Protect integration config and re-add it. If you do not have Smart Sensor devices, you can migrate to the Home Assistant core version by following the steps below:
1. Upgrade to the 0.12.0 version for the HACS unifiprotect integration and restart Home Assistant.
2. Remove your HACS `unifiprotect` integration from HACS (do not remove your `unifiprotect` config entry). It is safe to ignore the warning about needing to remove your config first.
3. Do *not* restart HA yet.
4. Upgrade to Home Assistant 2022.2.x
You **must** remove the HACS integration efore upgrading to 2022.2.0 first to prevent a conflicting version of `pyunifiprotect` from being installed.
### Differences between HACS version 0.12.0 and HA 2022.2.0b1 version:
#### HACS Only
* Migration code for updating from `0.10.x` or older still exists; this code has been _removed_ in the HA core version
#### HA Core Only
* Full language support. All of the languages HA core supports via Lokalise has been added to the ingration.
* Auto-discovery. If you have a Dream machine or a Cloud Key/UNVR on the same VLAN, the UniFi Protect integration will automatically be discovered and prompted for setup.
* UP Doorlock support. The HA core version has full support for the newly release EA UP Doorlock.
-----
 [](https://github.com/custom-components/hacs) [](https://community.home-assistant.io/t/custom-component-unifi-protect/158041)
The UniFi Protect Integration adds support for retrieving Camera feeds and Sensor data from a UniFi Protect installation on either an Ubiquiti CloudKey+, Ubiquiti UniFi Dream Machine Pro or UniFi Protect Network Video Recorder.
There is support for the following device types within Home Assistant:
* Camera
* A camera entity for each camera channel and RTSP(S) combination found on the NVR device will be created
* Sensor
* **Cameras**: (only for cameras with Smart Detections) Currently detected object
* **Sensors**: Sensors for battery level, light level, humidity and temperate
* **All Devices** (Disabled by default): a sensor for uptime, BLE signal (only for bluetooth devices), link speed (only for wired devices), WiFi signal (only for WiFi devices)
* **Cameras** (Disabled by default): sensors for bytes transferred, bytes received, oldest recording, storage used by camera recordings, write rate for camera recordings
* **Doorbells** (Disabled by default, requires UniFi Protect 1.20.1+) current voltage sensor
* **NVR** (Disabled by default): sensors for uptime, CPU utilization, CPU temp, memory utilization, storage utilization, percent distribution of timelapse, continuos, and detections video on disk, percentage of HD video, 4K video and free space of disk, estimated recording capacity
* Binary Sensor
* **Cameras** and **Flood Lights**: sensors for if it is dark, if motion is detected
* **Doorbells**: sensor if the doorbell is currently being rung
* **Sensors**: sensors for if the door is open, battery is low and if motion is detected
* **NVR** (Disabled by default): a sensor for the disk health for each disk
* **NOTE**: The disk numbers here are _not guaranteed to match up to the disk numbers shown in UniFiOS_
* Switch
* **Cameras**: switches to enabled/disable status light, HDR, High FPS mode, "Privacy Mode", System Sounds (if the camera has speakers), toggles for the Overlay information, toggles for smart detections objects (if the camera has smart detections)
* **Privacy Mode**: Turning on Privacy Mode adds a privacy zone that blacks out the camera so nothing can be seen, turn microphone sensitivity to 0 and turns off recording
* **Flood Lights**: switch to enable/disable status light
* **All Devices** (Disabled by default): Switch to enable/disable SSH access
* Light
* A light entity will be created for each UniFi Floodlight found. This works as a normal light entity, and has a brightness scale also.
* Select
* **Cameras**: selects to choose between the recording mode and the current infrared settings (if the camera has IR LEDs)
* **Doorbells**: select to choose between the currently disable text options on the LCD screen
* **Flood Lights**: select to choose between the light turn on mode and the paired camera (used for motion detections)
* **Viewports**: select to choose between the currently active Liveview display on the Viewport
* Number
* **Cameras**: number entities for the current WDR setting (only if the camera does not have HDR), current microphone sensitivity level, current optical zoom level (if camera has optical zoom),
* **Doorbells**: number entity for the current chime duration
* **Flood Lights**: number entities for the current motion sensitivity level and auto-shutdown duration after the light triggers on
* Media Player
* A media player entity is added for any camera that has speakers that allow talkback
* Button
* A button entity is added for every adoptable device (anything except the UniFiOS console) to allow you to reboot the device
It supports both regular Ubiquiti Cameras and the UniFi Doorbell. Camera feeds, Motion Sensors, Doorbell Sensors, Motion Setting Sensors and Switches will be created automatically for each Camera found, once the Integration has been configured.
## Table of Contents
1. [UniFi Protect Support](#unifi-protect-support)
2. [Hardware Support](#hardware-support)
3. [Prerequisites](#prerequisites)
4. [Installation](#installation)
5. [UniFi Protect Services](#special-unifi-protect-services)
6. [UniFi Protect Events](#unifi-protect-events)
7. [Automating Services](#automating-services)
* [Send a notification when the doorbell is pressed](#send-a-notification-when-the-doorbell-is-pressed)
* [Person Detection](#automate-person-detection)
* [Input Slider for Doorbell Chime Duration](#create-input-slider-for-doorbell-chime-duration)
8. [Enable Debug Logging](#enable-debug-logging)
9. [Contribute to Development](#contribute-to-the-project-and-developing-with-a-devcontainer)
## UniFi Protect Support
In general, stable/beta version of this integration mirror stable/beta versions of UniFi Protect. That means:
**Stable versions of this integration require the latest stable version of UniFi Protect to run.**
**Beta versions / `master` branch of this integration require the latest beta version of UniFi Protect to run (or the latest stable if there is no beta)**
We try our best to avoid breaking changes so you may need to use older versions of UniFi Protect with newer versions of the integration. Just keep in mind, we may not be able to support you if you do.
## Docs for Old Versions
If you are not using the latest beta of the integration, you can view old versions of this README at any time in GitHub at `https://github.com/briis/unifiprotect/tree/{VERSION}`. Example, docs for v0.9.1 can be found at [https://github.com/briis/unifiprotect/tree/v0.9.1](https://github.com/briis/unifiprotect/tree/v0.9.1)
## Minimal Versions
As of v0.10 of the integration, the following versions of HA and UniFi Protect are _required_ to even install the integration:
* UniFi Protect minimum version is **1.20.0**
* Home Assistant minimum version is **2021.11.0**
## Hardware Support
This Integration supports all UniFiOS Consoles that can run UniFi Protect. Currently this includes:
* UniFi Protect Network Video Recorder (**UNVR**)
* UniFi Protect Network Video Recorder Pro (**UNVRPRO**)
* UniFi Dream Machine Pro (**UDMP**)
* UniFi Cloud Key Gen2 Plus (**CKGP**) firmware version v2.0.24+
Ubiquity released V2.0.24 as an official firmware release for the CloudKey+, and it is recommended that people upgrade to this UniFiOS based firmware for their CloudKey+, as this gives a much better realtime experience.
CKGP with Firmware V1.x **do NOT run UniFiOS**, you must upgrade to firmware v2.0.24 or newer.
**NOTE**: If you are still running a version of UniFi Protect without a UniFiOS Console, you can use a V0.8.x as it is the last version fully supported by NON UniFiOS devices. However, please note NON UniFiOS devices are not supported by us anymore.
## Prerequisites
Before you install this Integration you need to ensure that the following two settings are applied in UniFi Protect:
1. **Local User**
* Login to your *Local Portal* on your UniFiOS device, and click on *Users*
* In the upper right corner, click on *Add User*
* Click *Add Admin*, and fill out the form. Specific Fields to pay attention to:
* Role: Must be *Limited Admin*
* Account Type: *Local Access Only*
* CONTROLLER PERMISSIONS - Under UniFi Protect, select Administrators.
* Click *Add* in at the bottom Right.
**HINT**: A few users have reported that they had to restart their UDMP device after creating the local user for it to work. So if you get some kind of *Error 500* when setting up the Integration, try restart the UDMP.

2. **RTSP Stream**
The Integration uses the RTSP Stream as the Live Feed source, so this needs to be enabled on each camera. With the latest versions of UniFi Protect, the stream is enabled per default, but it is recommended to just check that this is done. To check and enable the the feature
* open UniFi Protect and click on *Devices*
* Select *Manage* in the Menu bar at the top
* Click on the + Sign next to RTSP
* Enable minimum 1 stream out of the 3 available. UniFi Protect will select the Stream with the Highest resolution
## Installation
This Integration is part of the default HACS store. Search for *unifi protect* under *Integrations* and install from there. After the installation of the files you must restart Home Assistant, or else you will not be able to add UniFi Protect from the Integration Page.
If you are not familiar with HACS, or haven't installed it, I would recommend to [look through the HACS documentation](https://hacs.xyz/), before continuing. Even though you can install the Integration manually, I would recommend using HACS, as you would always be reminded when a new release is published.
**Please note**: All HACS does, is copying the needed files to Home Assistant, and placing them in the right directory. To get the Integration to work, you now need to go through the steps in the *Configuration* section.
Before you restart Home Assistant, make sure that the stream component is enabled. Open `configuration.yaml` and look for *stream:*. If not found add `stream:` somewhere in the file and save it.
## Configuration
To add *UniFi Protect* to your Home Assistant installation, go to the Integrations page inside the configuration panel, click on `+ ADD INTEGRATION`, find *UniFi Protect*, and add your UniFi Protect server by providing the Host IP, Port Number, Username and Password.
**Note**: If you can't find the *UniFi Protect* integration, hard refresh your browser, when you are on the Integrations page.
If the UniFi Protect Server is found on the network it will be added to your installation. After that, you can add more UniFi Protect Servers, should you have more than one installed.
**You can only add UniFi Protect through the Integration page, Yaml configuration is no longer supported.**
### MIGRATING FROM CLOUDKEY+ V1.x
When you upgrade your CloudKey+ from FW V1.x to 2.x, your CK wil move to UniFiOS as core operating system. That also means that where you previously used port 7443 you now need to use port 443. There are two ways to fix this:
* Delete the UniFi Protect Integration and re-add it, using port 443.
* Edit the file `.storage/core.config_entries` in your Home Assistant instance. Search for UniFi Protect and change port 7443 to 443. Restart Home Assistant. (Make a backup first)
### CONFIGURATION VARIABLES
**host**:
*(string)(Required)*
Type the IP address of your *UniFi Protect NVR*. Example: `192.168.1.1`
**port**:
*(int)(Optional)*
The port used to communicate with the NVR. Default is 443.
**username**:
*(string)(Required)*
The local username you setup under the *Prerequisites* section.
**password**:
*(string)(Required)*
The local password you setup under the *Prerequisites* section.
**verify ssl**:
*(bool)(Required)*
If your UniFi Protect instance has a value HTTPS cert, you can enforce validation of the cert
**deactivate rtsp stream**
*(bool)Optional*
If this box is checked, the camera stream will not use the RTSP stream, but instead jpeg push. This gives a realtime stream, but does not include Audio.
**realtime metrics**
*(bool)Optional*
Enable processing of all Websocket events from UniFi Protect. This enables realtime updates for many sensors that are disabled by default. If this is disabled, those sensors will only update once every 15 minutes. **Will greatly increase CPU usage**, do not enable unless you plan to use it.
**override connection host**
*(bool)Optional*
By default uses the connection host provided by your UniFi Protect instance for connecting to cameras for RTSP(S) streams. If you would like to force the integration to use the same IP address you provided above, set this to true.
## Special UniFi Protect Services
The Integration adds specific *UniFi Protect* services and supports the standard camera services. Below is a list of the *UniFi Protect* specific services:
Service | Parameters | Description
:------------ | :------------ | :-------------
`unifiprotect.add_doorbell_text` | `device_id` - A device for your current UniFi Protect instance (in case you have multiple).
`message` - custom message text to add| Adds a new custom message for Doorbells.\*
`unifiprotect.remove_doorbell_text` | `device_id` - A device for your current UniFi Protect instance (in case you have multiple).
`message` - custom message text to remove| Remove an existing custom message for Doorbells.\*
`unifiprotect.set_default_doorbell_text` | `device_id` - A device for your current UniFi Protect instance (in case you have multiple).
`message` - default text for doorbell| Sets the "default" text for when a message is reset or none is set.\*
`unifiprotect.set_doorbell_message` | `device_id` - A device for your current UniFi Protect instance (in case you have multiple).
`message` - text for doorbell| Dynamically sets text for doorbell.\*\*
`unifiprotect.profile_ws_messages` | `device_id` - A device for your current UniFi Protect instance (in case you have multiple).
`duration` - how long to provide| Debug service to help profile the processing of Websocket messages from UniFi Protect.
\*: Adding, removing or changing a doorbell text option requires you to restart your Home Assistant instance to be able to use the new ones. This is a limitation of how downstream entities and integrations subscribe to options for select entities. They cannot be dynamic.
\*\*: The `unifiprotect.set_doorbell_message` service should _only_ be used for setting the text of your doorbell dynamically. i.e. if you want to set the current time or outdoor temp on it. If you want to set a static message, use the select entity already provided. See the [Dynamic Doorbell](#dynamic-doorbell-messages) blueprint for an example.
## Automating Services
As part of the integration, we provide a couple of blueprints that you can use or extend to automate stuff.
### Doorbell Notifications
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fbriis%2Funifiprotect%2Fmaster%2Fblueprints%2Fautomation%2Funifiprotect%2Fpush_notification_doorbell_event.yaml)
### Motion Notifications
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fbriis%2Funifiprotect%2Fmaster%2Fblueprints%2Fautomation%2Funifiprotect%2Fpush_notification_motion_event.yaml)
### Smart Detection Notifications
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fbriis%2Funifiprotect%2Fmaster%2Fblueprints%2Fautomation%2Funifiprotect%2Fpush_notification_smart_event.yaml)
### Dynamic Doorbell Messages
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fbriis%2Funifiprotect%2Fmaster%2Fblueprints%2Fautomation%2Funifiprotect%2Fdynamic_doorbell.yaml)
### Enable Debug Logging
If logs are needed for debugging or reporting an issue, use the following configuration.yaml:
```yaml
logger:
default: error
logs:
pyunifiprotect: debug
custom_components.unifiprotect: debug
```
### CONTRIBUTE TO THE PROJECT AND DEVELOPING WITH A DEVCONTAINER
1. Fork and clone the repository.
2. Open in VSCode and choose to open in devcontainer. Must have VSCode devcontainer prerequisites.
3. Run the command container start from VSCode terminal
4. A fresh Home Assistant test instance will install and will eventually be running on port 9123 with this integration running
5. When the container is running, go to http://localhost:9123 and the add UniFi Protect from the Integration Page.
================================================
FILE: blueprints/automation/unifiprotect/dynamic_doorbell.yaml
================================================
blueprint:
name: UniFi Protect Dynamic Doorbell
description: |
## UniFi Protect Dynamic Doorbell
This blueprint will dynamically update the text on the LCD display for your UniFi Protect Doorbell. Will automatically run once per minute and update your Doorbell LCD Screen.
For this automation to run, you need to ensure your doorbell LCD display is set to the "Default Message" option. This is to still allow you to set static custom messages like "LEAVE PACKAGE AT THE DOOR" without being overridden by the automation. This behavior can be disabled in the options.
### Required Settings
- UniFi Protect Doorbell Sensor
### Optional Settings
- Text template format you want to display (see more below)
- Temperature sensor entity so you can display temperature on screen
- Time formatting string for formatting `{ctime}` template (see more below)
### Requirements
To take full effect of this automation blueprint, your Home Assistant instance needs some setup beforehand.
- A UniFi Protect NVR running on a UDM Pro, UNVR or other Protect Console
- The [unifiprotect][1] integration version 0.11-beta4 or newer
- A UniFi G4 Doorbell
### Text Template
The text that is display on your doorbell is configurable using the templating engine. Any [Home Assistant Templating][2] _should_ work in the template.
The follow variables will injected and replaced at render time as well (note the _single_ curly bracket, not 2):
- `{ctime}` -- current time, formatting according to "Time Format String" option
- `{temp}` -- current temperature from the "Temperature Sensor Entity" option
[1]: https://community.home-assistant.io/t/custom-component-unifi-protect/158041
[2]: https://www.home-assistant.io/docs/configuration/templating/
domain: automation
input:
doorbell:
name: Doorbell Entity
description: >
The doorbell sensor you want to trigger notifications for.
selector:
entity:
integration: unifiprotect
domain: select
temp_entity:
name: (Optional) Temperature Sensor Entity
description: Temperature sensor to use. Adds `{temp}` var to template.
default: ""
selector:
entity:
domain: sensor
device_class: temperature
text_template:
name: (Optional) Text Template
description: >
Message template to display on doorbell. Can be any HA template string.
Final generated string must be 30 characters or less.
default: "Welcome | {ctime}"
selector:
text:
always_run:
name: (Optional) Ignore current state?
description: >
Ignore the current state of the doorbell text message. If true, will always run every
minute, potentially resetting a custom static message you set. If false, will only
run every minute if the current text is the "Default Message" option or the
"unknown" (meaning it is already set to a dynamic value).
default: false
selector:
boolean:
time_format:
name: (Optional) Time Format String
description: >
Python datetime format code string for the `{ctime}` variable.
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
default: "%I:%M %p"
selector:
text:
mode: single
max_exceeded: silent
variables:
# input vars
input_doorbell: !input doorbell
input_time_format: !input time_format
input_text_template: !input text_template
input_temp_entity: !input temp_entity
input_always_run: !input always_run
trigger:
- platform: time_pattern
minutes: "*"
condition:
- "{{ input_always_run or is_state(input_doorbell, 'unknown') or states[input_doorbell].state.startswith('Default Message') }}"
action:
- service: unifiprotect.set_doorbell_message
data:
entity_id: !input doorbell
message: |
{%- set ctime=as_local(now()).strftime(input_time_format) -%}
{%- if input_temp_entity != "" -%}
{%- if is_state(input_temp_entity, 'unavailable') -%}
{%- set temp="" %}
{%- else -%}
{%- set temp="{}{}".format(int(states[input_temp_entity].state), state_attr(input_temp_entity, "unit_of_measurement")) -%}
{%- endif -%}
{%- endif -%}
{{ input_text_template.replace("{ctime}", ctime).replace("{temp}", temp) }}
================================================
FILE: blueprints/automation/unifiprotect/push_notification_doorbell_event.yaml
================================================
blueprint:
name: UniFi Protect Doorbell Notifications
description: |
## UniFi Protect Doorbell Notifications
This blueprint will send push notifications to desktop browser / mobile Home Assistant apps / Telegram when a UniFi Chime is rung.
### Required Settings
- UniFi Protect Doorbell Sensor
### Optional Settings
- [HTML5 Push Notification Target][1] and/or [Mobile App Notification Target][2]
- Notification targets and toggles for following notifications types:
- [HTML5 Push Notification][1]
- [Mobile App Notification][2]
- [Telegram Notification][9]
- Time formatting strings. Timestamp is injected into the notification in case the notification is delay.
- Cooldown before sending another notification
- Silence timer for muting notifications via Actionable Notification (docs: [HTML5][6], [Mobile][7])
- Configurable HA Internal / External Base URLs
- Configurable lovelace view from notification
- Optional Actionable Notification to unlock door entity for camera
- Optional TTS messages (requires [TTS integration][10] to be configured):
- TTS message to play through the doorbell when unlocking door
- Actionable Notification to play audio message through doorbell to respond to guest.
### Requirements
To take full effect of this automation blueprint, your Home Assistant instance needs some setup beforehand.
- A UniFi Protect NVR running on a UDM Pro, UNVR or other Protect Console
- The [unifiprotect][8] integration (requires version 0.11.1 or newer for TTS)
- A UniFi camera pair with your NVR that has a chime (like the G4 Doorbell)
- A valid HTTPS certificate and public facing Home Assistant instance
- If you do not have these, the actionable notifications and images will not appear in the notifications.
- You do not need your _whole_ Home Assistant to be publicly accessible. Only the paths `/api/camera_proxy/*` and `/api/webhook/*` need to be accessible outside of your network.
- TTS messages require Home Assistant to be able to communicate to your cameras directly (**not** to your NVR). It will _not_ work if the internal LAN IP of the camera is not routable by Home Assistant.
### Caveat About Actionable Notifications Limits
HTML5 Push notification can only have a max of 2 actionable notifications. If you enable Slience, Unlock Door and Respond, Slience will not appear.
Android Push notifications likewise have a limit of 3 actionable notifications. So if you have Open Camera, Slience, Unlock Door and Respond, Slience will not appear.
iOS is not affected by the limit (limit = 10).
[1]: https://www.home-assistant.io/integrations/html5
[2]: https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
[3]: https://www.home-assistant.io/integrations/html5#tag
[4]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#notification-channels
[5]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing
[6]: https://www.home-assistant.io/integrations/html5#actions
[7]: https://companion.home-assistant.io/docs/notifications/actionable-notifications/
[8]: https://community.home-assistant.io/t/custom-component-unifi-protect/158041
[9]: https://www.home-assistant.io/integrations/telegram/
[10]: https://www.home-assistant.io/integrations/tts/
domain: automation
input:
doorbell:
name: Doorbell Entity
description: >
The doorbell sensor you want to trigger notifications for.
selector:
entity:
integration: unifiprotect
domain: binary_sensor
device_class: occupancy
lock_entity:
name: (Optional) Door Lock Entity
description: >
The Lock entity to provide an actionable notification to unlock on doorbell
ring. The time interval you have to respond to the unlock action is controlled
by "Cooldown". Short Cooldown timers may prevent you from unlocking the door.
default: ""
selector:
entity:
domain: lock
tts_target:
name: (Optional) TTS Service
description: >
The TTS service you want to use to generate TTS messages
https://www.home-assistant.io/integrations/tts/
default: ""
selector:
text:
lock_tts:
name: (Optional) Unlock TTS message
description: >
TTS Message for play through doorbell when door is unlocked.
default: ""
selector:
text:
wait_tts:
name: (Optional) TTS message
description: >
Adds actionable notification to play TTS message to respond to guest. The time
interval you have to respond to the TTS action is controlled by "Cooldown".
Short Cooldown timers may prevent you from sending a TTS message the door.
default: ""
selector:
text:
send_mobile:
name: (Optional) Send Mobile App Notifications
description: Send mobile app push notifications
default: true
selector:
boolean:
notify_target_app:
name: (Optional) Notification Target (Mobile App)
description: >
The notification target for mobile apps notifications. Should be only the
specific service name in the notify domain.
https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
default: notify
selector:
text:
send_html5:
name: (Optional) Send HTML5 Notifications
description: >
Send HTML5 push notifications. Requires you to have configured push
notifications on at least one device.
default: false
selector:
boolean:
notify_target_html5:
name: (Optional) Notification Target (HTML5 Push)
description: >
The notification target for HTML5 push notifications. Should be only the
specific service name in the notify domain.
https://www.home-assistant.io/integrations/html5
default: push_notification
selector:
text:
channel:
name: (Optional) Notification Channel
description: >
Notification channel/tag to use. Will automatically be prepended with
"Manual " if action is triggered manually.
https://companion.home-assistant.io/docs/notifications/notifications-basic#notification-channels
default: Doorbell
selector:
text:
send_telegram:
name: (Optional) Telegram Notification
description: >
Send a notification via Telegram. Telegram notification will not have a link to Home Assistant like the mobile apps.
default: false
selector:
boolean:
notify_telegram:
name: (Optional) Notification Target (Telegram)
description: >
The notification target for Telegram notifications. Should be name of the Telegram bot you have configured.
https://www.home-assistant.io/integrations/telegram/
default: telegrambot
selector:
text:
time_format:
name: (Optional) Time Format String
description: >
Python datetime format code string for the event trigger time. This string is
the actual time the doorbell event was triggered in case the automation or
notification is delayed. Manual triggers will cause this to always be the time
of the previous event.
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
default: "%I:%M %p"
selector:
text:
cooldown:
name: (Optional) Cooldown
description: >
Delay before sending another notification for this camera after the last event.
Is also the interval you have to respond to actions in notification.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: seconds
silence_timer:
name: (Optional) Silence Notifications
description: >
How long to silence notifications for this camera when requested as part of the
actionable notification. The time interval you have to respond to the slient
action is controlled by "Cooldown". Short Cooldown timers may prevent you from
silencing.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: minutes
base_ha_url:
name: (Optional) Base Home Assistant URL
description: Base URL to use for opening HA links in HTML5 push notifications.
default: http://homeassistant.local:8123
selector:
text:
base_image_url:
name: (Optional) Base Image URL
description: >
Publicly accessible base URL for your Home Assistant instance. If you are using
Nabu Casa, it should be that URL. May be different from your Base Home Assistant
URL if your HA instance not publicly accessible.
Must be an HTTPS URL with a valid certificate.
default: ""
selector:
text:
lovelace_view:
name: (Optional) Lovelace View
description: |
Home Assistant Lovelace view to open when clicking notification.
If left blank, URI Notification actions will not be generated.
default: ""
selector:
text:
debug_enabled:
name: (Optional) Debug
description: >
Enable debugging for automation. If enabled, will send persistent notifications
with extra data.
default: false
selector:
boolean:
mode: single
max_exceeded: silent
variables:
# input vars
input_doorbell: !input doorbell
input_channel: !input channel
input_base_image_url: !input base_image_url
input_base_ha_url: !input base_ha_url
input_lovelace_view: !input lovelace_view
input_debug_enabled: !input debug_enabled
input_notify_target_app: !input notify_target_app
input_notify_target_html5: !input notify_target_html5
input_notify_telegram: !input notify_telegram
input_silence_timer: !input silence_timer
input_lock_entity: !input lock_entity
input_send_mobile: !input send_mobile
input_send_html5: !input send_html5
input_send_telegram: !input send_telegram
input_time_format: !input time_format
input_lock_tts: !input lock_tts
input_wait_tts: !input wait_tts
input_tts_target: !input tts_target
# automation data
camera_entities: '[{% for eid in device_entities(device_id(input_doorbell)) %}{%if eid.startswith(''camera'') and not is_state(eid, ''unavailable'') %}"{{ eid }}",{% endif %}{% endfor %}]'
media_entities: '[{% for eid in device_entities(device_id(input_doorbell)) %}{%if eid.startswith(''media_player'') and not is_state(eid, ''unavailable'') %}"{{ eid }}",{% endif %}{% endfor %}]'
# automation variables
lovelace_view: "{{ input_lovelace_view | trim }}"
camera_entity_id: "{{ camera_entities | default([None]) | first }}"
media_entity_id: "{{ media_entities | default([None]) | first }}"
lock_entity_id: "{{ input_lock_entity or '' }}"
trigger_time: |
{% if states[input_doorbell] == None %}
None
{% else %}
{{ as_local(states[input_doorbell].last_changed).strftime(input_time_format) }}
{% endif %}
notification_channel: |
{% if "from_state" in trigger %}
{{ input_channel }}
{% else %}
Manual {{ input_channel }}
{% endif %}
notification_tag: "{{ notification_channel.lower().replace(' ', '-') }}"
notification_title: "{{ device_attr(input_doorbell, 'name') }}"
notification_url: |
{% if lovelace_view == "" %}
None
{% else %}
{{ input_base_ha_url | trim }}{{ lovelace_view }}
{% endif %}
notification_message: "Someone rang {{ notification_title }}{% if trigger_time != None %} at {{ trigger_time }}{% endif %}."
notification_message_html5: |
{{ notification_message }}{% if notification_url != None %}
Tap to open camera in Home Assistant.
{% endif %}
notification_image: |
{% if camera_entity_id == None or input_base_image_url == "" %}
None
{% else %}
{{ input_base_image_url | trim }}{{ state_attr(camera_entity_id, 'entity_picture') }}
{% endif %}
silence_action: "silence-{{ input_doorbell }}"
unlock_action: "unlock-{{ lock_entity_id }}"
tts_action: "tts-{{ input_doorbell }}"
lock_tts_enabled: "{{ input_tts_target != '' and input_lock_tts != '' and media_entity_id != None }}"
wait_tts_enabled: "{{ input_tts_target != '' and input_wait_tts != '' and media_entity_id != None }}"
trigger:
- platform: state
entity_id: !input doorbell
from: "off"
to: "on"
action:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: |
Entity ID: `{{ input_doorbell }}`
Camera: `{{ camera_entity_id }}`
Media Player: `{{ media_entity_id }}`
Lock: `{{ lock_entity_id }}`
Lock TTS: `{{ input_lock_tts }}` | `{{ lock_tts_enabled }}`
Wait TTS: `{{ input_wait_tts }}` | `{{ wait_tts_enabled }}`
TTS Service: `{{ input_tts_target }}`
Notification Service (Mobile): `notify.{{ input_notify_target_app }}`
Notification Service (HTML5): `notify.{{ input_notify_target_html5 }}`
Notification Service (Telegram): `notify.{{ input_notify_telegram }}`
Channel: {{ notification_channel }}
Tag: {{ notification_tag }}
Message: {{ notification_message }}
Image: {{ notification_image }}
URL: {{ notification_url }}
- choose:
- conditions: "{{ input_send_mobile }}"
sequence:
- service: notify.{{ input_notify_target_app }}
data:
message: "{{ notification_message }}"
title: "{{ notification_title }}"
data:
# Android/iOS notification tag
tag: "{{ notification_tag }}"
# Android notification Channel
channel: "{{ notification_channel }}"
# Android high prority
ttl: 0
priority: high
# iOS high prority
time-sensitive: 1
# Android image
image: "{{ notification_image }}"
# iOS image
attachment:
url: "{{ notification_image }}"
actions: >
[
{% if notification_url != None %}
{ "action": "URI", "title": "Open Camera", "uri": "{{ lovelace_view }}" },
{% endif %}
{% if lock_entity_id != "" %}
{ "action": "{{ unlock_action }}", "title": "Unlock Door" },
{% endif %}
{% if wait_tts_enabled %}
{ "action": "{{ tts_action }}", "title": "Respond" },
{% endif %}
{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence", "destructive": True },
{% endif %}
]
- choose:
- conditions: "{{ input_send_html5 }}"
sequence:
- service: notify.{{ input_notify_target_html5 }}
data:
message: "{{ notification_message_html5 }}"
title: "{{ notification_title }}"
data:
# HTML5 Notification tag
tag: "{{ notification_tag }}"
image: "{{ notification_image }}"
url: "{{ notification_url }}"
actions: >
[
{% if lock_entity_id != "" %}
{ "action": "{{ unlock_action }}", "title": "Unlock Door" },
{% endif %}
{% if wait_tts_enabled %}
{ "action": "{{ tts_action }}", "title": "Respond" },
{% endif %}
{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence" },
{% endif %}
]
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: notify.{{ input_notify_telegram }}
data:
title: "{{ notification_title }}"
message: "{{ notification_message }}"
data: >
{
{%- if notification_image != None -%}
"photo": {
"url": "{{ notification_image }}",
"caption": "{{ notification_message }}",
"message_tag": "{{ notification_tag }}",
},
{%- endif -%}
"inline_keyboard": [
{% if lock_entity_id != "" %}
"Unlock Door:/{{ unlock_action }}",
{% endif %}
{% if wait_tts_enabled %}
"Respond:/{{ tts_action }}",
{% endif %}
{%- if input_silence_timer > 0 -%}
"Silence:/{{ silence_action }}"
{%- endif -%}
]
}
- wait_for_trigger:
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: html5_notification.clicked
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: telegram_callback
event_data:
data: "/{{ silence_action }}"
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ unlock_action }}"
- platform: event
event_type: html5_notification.clicked
event_data:
action: "{{ unlock_action }}"
- platform: event
event_type: telegram_callback
event_data:
data: "/{{ unlock_action }}"
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ tts_action }}"
- platform: event
event_type: html5_notification.clicked
event_data:
action: "{{ tts_action }}"
- platform: event
event_type: telegram_callback
event_data:
data: "/{{ tts_action }}"
timeout:
seconds: !input cooldown
continue_on_timeout: false
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: |
Callback: `{{ wait.trigger.event.event_type }}`
Action : `{{ wait.trigger.event.data.action | default("") }}`
Action (Telegram): `{{ wait.trigger.event.data.data | default("") }}`
- choose:
- conditions: "{{ (wait.trigger.event.data.action == unlock_action or wait.trigger.event.data.data.endswith(unlock_action)) and lock_entity_id != '' }}"
sequence:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: "Unlock `{{ lock_entity_id }}`"
- service: lock.unlock
data:
entity_id: "{{ lock_entity_id }}"
- choose:
- conditions: "{{ lock_tts_enabled }}"
sequence:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: "Sending TTS: {{ input_lock_tts }}."
- service: tts.{{ input_tts_target }}
data:
entity_id: "{{ media_entity_id }}"
message: "{{ input_lock_tts }}"
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: telegram_bot.answer_callback_query
data:
message: 'Unlocked door{% if lock_tts_enabled %} and played "{{ input_lock_tts }}" TTS message{% endif %}'
callback_query_id: "{{ wait.trigger.event.data.id }}"
- conditions: "{{ (wait.trigger.event.data.action == silence_action or wait.trigger.event.data.data.endswith(silence_action)) }}"
sequence:
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: telegram_bot.answer_callback_query
data:
message: "Doorbell notifications silenced for {{ input_silence_timer }} minutes"
callback_query_id: "{{ wait.trigger.event.data.id }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence started.
- delay:
minutes: "{{ input_silence_timer }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence ended.
- conditions: "{{ (wait.trigger.event.data.action == tts_action or wait.trigger.event.data.data.endswith(tts_action)) and wait_tts_enabled }}"
sequence:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: "Sending TTS: {{ input_wait_tts }}."
- service: tts.{{ input_tts_target }}
data:
entity_id: "{{ media_entity_id }}"
message: "{{ input_wait_tts }}"
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: telegram_bot.answer_callback_query
data:
message: 'Played "{{ input_wait_tts }}" TTS message'
callback_query_id: "{{ wait.trigger.event.data.id }}"
================================================
FILE: blueprints/automation/unifiprotect/push_notification_motion_event.yaml
================================================
blueprint:
name: UniFi Protect Motion Notifications
description: |
## UniFi Protect Motion Notifications
This blueprint will send push notifications to desktop browser / mobile Home Assistant / Telegram apps when a motion event is fired.
This blueprint is _only_ for **motion** events, not smart detections.
### Required Settings
- UniFi Protect Motion Sensor
### Optional Settings
- Precense filter for only sending notifications for when you are not home
- Notification targets and toggles for following notifications types:
- [HTML5 Push Notification][1]
- [Mobile App Notification][2]
- [Telegram Notification][9]
- Time formatting strings. Timestamp is injected into the notification in case the notification is delay.
- Notification Channel / Tag (docs: [HTML5 Tag][3], [Android Channels][4], [Mobile Tag][5])
- Cooldown before sending another notification
- Silence timer for muting notifications via Actionable Notification (docs: [HTML5][6], [Mobile][7])
- Configurable HA Internal / External Base URLs
- Configurable Lovelace view from notification
### Requirements
To take full effect of this automation blueprint, your Home Assistant instance needs some setup beforehand.
- A UniFi Protect NVR running on a UDM Pro, UNVR or other Protect Console
- The [unifiprotect][8] integration version 0.11.0 or newer
- A UniFi camera pair with your NVR
- A valid HTTPS certificate and public facing Home Assistant instance
- If you do not have these, the actionable notifications and images will not appear in the notifications.
- You do not need your _whole_ Home Assistant to be publicly accessible. Only the paths `/api/camera_proxy/*` and`/api/webhook/*` need to be accessible outside of your network.
[1]: https://www.home-assistant.io/integrations/html5
[2]: https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
[3]: https://www.home-assistant.io/integrations/html5#tag
[4]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#notification-channels
[5]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing
[6]: https://www.home-assistant.io/integrations/html5#actions
[7]: https://companion.home-assistant.io/docs/notifications/actionable-notifications/
[8]: https://community.home-assistant.io/t/custom-component-unifi-protect/158041
[9]: https://www.home-assistant.io/integrations/telegram/
domain: automation
input:
motion_entity:
name: Motion Entity
description: >
The camera motion sensor for you want to fire events for.
selector:
entity:
integration: unifiprotect
domain: binary_sensor
device_class: motion
presence_filter:
name: (Optional) Presence Filter
description: Only notify if selected presence entity is not "home".
default: ""
selector:
entity:
send_mobile:
name: (Optional) Send Mobile App Notifications
description: Send mobile app push notifications
default: true
selector:
boolean:
notify_target_app:
name: (Optional) Notification Target (Mobile App)
description: >
The notification target for mobile apps notifications. Should be only the
specific service name in the notify domain.
https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
default: notify
selector:
text:
send_html5:
name: (Optional) Send HTML5 Notifications
description: >
Send HTML5 push notifications. Requires you to have configured push
notifications on at least one device.
default: false
selector:
boolean:
notify_target_html5:
name: (Optional) Notification Target (HTML5 Push)
description: >
The notification target for HTML5 push notifications. Should be only the
specific service name in the notify domain.
https://www.home-assistant.io/integrations/html5
default: push_notification
selector:
text:
channel:
name: (Optional) Notification Channel
description: >
Notification channel/tag to use. Will automatically be prepended with
"Manual " if action is triggered manually.
https://companion.home-assistant.io/docs/notifications/notifications-basic#notification-channels
default: Motion
selector:
text:
send_telegram:
name: (Optional) Telegram Notification
description: >
Send a notification via Telegram. Telegram notification will not have a link to Home Assistant like the mobile apps.
default: false
selector:
boolean:
notify_telegram:
name: (Optional) Notification Target (Telegram)
description: >
The notification target for Telegram notifications. Should be name of the Telegram bot you have configured.
https://www.home-assistant.io/integrations/telegram/
default: telegrambot
selector:
text:
time_format:
name: (Optional) Time Format String
description: >
Python datetime format code string for the event trigger time. This string is
the actual time the motion event was triggered in case the automation or
notification is delayed. Manual triggers will cause this to always be the time
of the previous event.
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
default: "%I:%M %p"
selector:
text:
cooldown:
name: (Optional) Cooldown
description: >
Delay before sending another notification for this camera after the last event.
Is also the interval you have to respond to actions in notification.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: seconds
silence_timer:
name: (Optional) Silence Notifications
description: >
How long to silence notifications for this camera when requested as part of the
actionable notification. The time interval you have to respond to the slient
action is controlled by "Cooldown". Short Cooldown timers may prevent you from
silencing.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: minutes
base_ha_url:
name: (Optional) Base Home Assistant URL
description: Base URL to use for opening HA links in HTML5 push notifications.
default: http://homeassistant.local:8123
selector:
text:
base_image_url:
name: (Optional) Base Image URL
description: >
Publicly accessible base URL for your Home Assistant instance. If you are using
Nabu Casa, it should be that URL. May be different from your Base Home Assistant
URL if your HA instance not publicly accessible.
Must be an HTTPS URL with a valid certificate.
default: ""
selector:
text:
lovelace_view:
name: (Optional) Lovelace View
description: |
Home Assistant Lovelace view to open when clicking notification.
If left blank, URI Notification actions will not be generated.
default: ""
selector:
text:
debug_enabled:
name: (Optional) Debug
description: >
Enable debugging for automation. If enabled, will send persistent notifications
with extra data.
default: false
selector:
boolean:
mode: single
max_exceeded: silent
variables:
# input vars
input_motion_entity: !input motion_entity
input_channel: !input channel
input_base_image_url: !input base_image_url
input_base_ha_url: !input base_ha_url
input_lovelace_view: !input lovelace_view
input_debug_enabled: !input debug_enabled
input_notify_target_app: !input notify_target_app
input_notify_target_html5: !input notify_target_html5
input_notify_telegram: !input notify_telegram
input_silence_timer: !input silence_timer
input_send_mobile: !input send_mobile
input_send_html5: !input send_html5
input_send_telegram: !input send_telegram
input_time_format: !input time_format
input_presence_filter: !input presence_filter
# automation data
camera_entities: '[{% for eid in device_entities(device_id(input_motion_entity)) %}{%if eid.startswith(''camera'') and not is_state(eid, ''unavailable'') %}"{{ eid }}",{% endif %}{% endfor %}]'
# automation variables
lovelace_view: "{{ input_lovelace_view | trim }}"
camera_entity_id: "{{ camera_entities | default([None]) | first }}"
trigger_time: |
{% if states[input_motion_entity] == None %}
None
{% else %}
{{ as_local(states[input_motion_entity].last_changed).strftime(input_time_format) }}
{% endif %}
notification_channel: |
{% if "from_state" in trigger %}
{{ input_channel }}
{% else %}
Manual {{ input_channel }}
{% endif %}
notification_tag: "{{ notification_channel.lower().replace(' ', '-') }}"
notification_title: "{{ device_attr(input_motion_entity, 'name') }}"
notification_url: |
{% if lovelace_view == "" %}
None
{% else %}
{{ input_base_ha_url | trim }}{{ lovelace_view }}
{% endif %}
notification_message: "Motion detected by {{ notification_title }}{% if trigger_time != None %} at {{ trigger_time }}{% endif %}."
notification_message_html5: |
{{ notification_message }}{% if notification_url != None %}
Tap to open camera in Home Assistant.
{% endif %}
notification_image: |
{% if camera_entity_id == None or input_base_image_url == "" %}
None
{% else %}
{{ input_base_image_url | trim }}{{ state_attr(camera_entity_id, 'entity_picture') }}
{% endif %}
silence_action: "silence-{{ input_motion_entity }}"
trigger:
- platform: state
entity_id: !input motion_entity
from: "off"
to: "on"
condition:
- "{{ not input_presence_filter or not is_state(input_presence_filter, 'home') }}"
action:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: |
Entity ID: `{{ input_motion_entity }}`
Camera: `{{ camera_entity_id }}`
Notification Service (Mobile): `notify.{{ input_notify_target_app }}`
Notification Service (HTML5): `notify.{{ input_notify_target_html5 }}`
Notification Service (Telegram): `notify.{{ input_notify_telegram }}`
Channel: {{ notification_channel }}
Tag: {{ notification_tag }}
Message: {{ notification_message }}
Image: {{ notification_image }}
URL: {{ notification_url }}
- choose:
- conditions: "{{ input_send_mobile }}"
sequence:
- service: notify.{{ input_notify_target_app }}
data:
message: "{{ notification_message }}"
title: "{{ notification_title }}"
data:
# Android/iOS notification tag
tag: "{{ notification_tag }}"
# Android notification Channel
channel: "{{ notification_channel }}"
# Android high prority
ttl: 0
priority: high
# iOS high prority
time-sensitive: 1
# Android image
image: "{{ notification_image }}"
# iOS image
attachment:
url: "{{ notification_image }}"
actions: >
[{% if notification_url != None %}
{ "action": "URI", "title": "Open Camera", "uri": "{{ lovelace_view }}" },
{% endif %}
{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence", "destructive": True },
{% endif %}]
- choose:
- conditions: "{{ input_send_html5 }}"
sequence:
- service: notify.{{ input_notify_target_html5 }}
data:
message: "{{ notification_message_html5 }}"
title: "{{ notification_title }}"
data:
# HTML5 Notification tag
tag: "{{ notification_tag }}"
image: "{{ notification_image }}"
url: "{{ notification_url }}"
actions: >
[{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence" },
{% endif %}]
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: notify.{{ input_notify_telegram }}
data:
title: "{{ notification_title }}"
message: "{{ notification_message }}"
data: >
{
{%- if notification_image != None -%}
"photo": {
"url": "{{ notification_image }}",
"caption": "{{ notification_message }}",
"message_tag": "{{ notification_tag }}",
},
{%- endif -%}
{%- if input_silence_timer > 0 -%}
"inline_keyboard": ["Silence:/{{ silence_action }}"],
{%- endif -%}
}
- wait_for_trigger:
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: html5_notification.clicked
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: telegram_callback
event_data:
data: "/{{ silence_action }}"
timeout:
seconds: !input cooldown
continue_on_timeout: false
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: telegram_bot.answer_callback_query
data:
message: "Motion notifications silenced for {{ input_silence_timer }} minutes"
callback_query_id: "{{ wait.trigger.event.data.id }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence started.
- delay:
minutes: "{{ input_silence_timer }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence ended.
================================================
FILE: blueprints/automation/unifiprotect/push_notification_smart_event.yaml
================================================
blueprint:
name: UniFi Protect Smart Detection Notifications
description: |
## UniFi Protect Smart Detection Notifications
This blueprint will send push notifications to desktop browser / mobile Home Assistant / Telegram apps when a smart detection event is fired.
This blueprint is _only_ for **smart detection**, not motion.
### Required Settings
- UniFi Protect Smart Detection Sensor
- What Smart Detection(s) you want to trigger on
### Optional Settings
- Precense filter for only sending notifications for when you are not home
- Notification targets and toggles for following notifications types:
- [HTML5 Push Notification][1]
- [Mobile App Notification][2]
- [Telegram Notification][9]
- Time formatting strings. Timestamp is injected into the notification in case the notification is delay.
- Notification Channel / Tag (docs: [HTML5 Tag][3], [Android Channels][4], [Mobile Tag][5])
- Cooldown before sending another notification
- Silence timer for muting notifications via Actionable Notification (docs: [HTML5][6], [Mobile][7])
- Configurable HA Internal / External Base URLs
- Configurable Lovelace view from notification
### Requirements
To take full effect of this automation blueprint, your Home Assistant instance needs some setup beforehand.
- A UniFi Protect NVR running on a UDM Pro, UNVR or other Protect Console
- The [unifiprotect][8] integration version 0.11.0 or newer
- A UniFi camera pair with your NVR that has Smart Detections. This is any G4 series camera _except_ the EA G4 Instant.
- A valid HTTPS certificate and public facing Home Assistant instance
- If you do not have these, the actionable notifications and images will not appear in the notifications.
- You do not need your _whole_ Home Assistant to be publicly accessible. Only the paths `/api/camera_proxy/*` and`/api/webhook/*` need to be accessible outside of your network.
[1]: https://www.home-assistant.io/integrations/html5
[2]: https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
[3]: https://www.home-assistant.io/integrations/html5#tag
[4]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#notification-channels
[5]: https://companion.home-assistant.io/docs/notifications/notifications-basic/#replacing
[6]: https://www.home-assistant.io/integrations/html5#actions
[7]: https://companion.home-assistant.io/docs/notifications/actionable-notifications/
[8]: https://community.home-assistant.io/t/custom-component-unifi-protect/158041
[9]: https://www.home-assistant.io/integrations/telegram/
domain: automation
input:
smart_entity:
name: Smart Dection Entity
description: >
The smart detection sensor for you want to fire events for.
selector:
entity:
integration: unifiprotect
domain: sensor
device_class: occupancy
objects:
name: (Optional) Smart Detections
description: >
Smart Detections to filter on. List should be comma separated.
Possible objects: person, vehicle
default: person,vehicle
selector:
text:
presence_filter:
name: (Optional) Presence Filter
description: Only notify if selected presence entity is not "home".
default: ""
selector:
entity:
send_mobile:
name: (Optional) Send Mobile App Notifications
description: Send mobile app push notifications
default: true
selector:
boolean:
notify_target_app:
name: (Optional) Notification Target (Mobile App)
description: >
The notification target for mobile apps notifications. Should be only the
specific service name in the notify domain.
https://companion.home-assistant.io/docs/notifications/notifications-basic#sending-notifications-to-multiple-devices
default: notify
selector:
text:
send_html5:
name: (Optional) Send HTML5 Notifications
description: >
Send HTML5 push notifications. Requires you to have configured push
notifications on at least one device.
default: false
selector:
boolean:
notify_target_html5:
name: (Optional) Notification Target (HTML5 Push)
description: >
The notification target for HTML5 push notifications. Should be only the
specific service name in the notify domain.
https://www.home-assistant.io/integrations/html5
default: push_notification
selector:
text:
channel:
name: (Optional) Notification Channel
description: >
Notification channel/tag to use. Will automatically be prepended with
"Manual " if action is triggered manually.
https://companion.home-assistant.io/docs/notifications/notifications-basic#notification-channels
default: Smart Detection
selector:
text:
send_telegram:
name: (Optional) Telegram Notification
description: >
Send a notification via Telegram. Telegram notification will not have a link to Home Assistant like the mobile apps.
default: false
selector:
boolean:
notify_telegram:
name: (Optional) Notification Target (Telegram)
description: >
The notification target for Telegram notifications. Should be name of the Telegram bot you have configured.
https://www.home-assistant.io/integrations/telegram/
default: telegrambot
selector:
text:
time_format:
name: (Optional) Time Format String
description: >
Python datetime format code string for the event trigger time. This string is
the actual time the motion event was triggered in case the automation or
notification is delayed. Manual triggers will cause this to always be the time
of the previous event.
https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes
default: "%I:%M %p"
selector:
text:
cooldown:
name: (Optional) Cooldown
description: >
Delay before sending another notification for this camera after the last event.
Is also the interval you have to respond to actions in notification.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: seconds
silence_timer:
name: (Optional) Silence Notifications
description: >
How long to silence notifications for this camera when requested as part of the
actionable notification. The time interval you have to respond to the slient
action is controlled by "Cooldown". Short Cooldown timers may prevent you from
silencing.
default: 30
selector:
number:
max: 300
min: 0
unit_of_measurement: minutes
base_ha_url:
name: (Optional) Base Home Assistant URL
description: Base URL to use for opening HA links in HTML5 push notifications.
default: http://homeassistant.local:8123
selector:
text:
base_image_url:
name: (Optional) Base Image URL
description: >
Publicly accessible base URL for your Home Assistant instance. If you are using
Nabu Casa, it should be that URL. May be different from your Base Home Assistant
URL if your HA instance not publicly accessible.
Must be an HTTPS URL with a valid certificate.
default: ""
selector:
text:
lovelace_view:
name: (Optional) Lovelace View
description: |
Home Assistant Lovelace view to open when clicking notification.
If left blank, URI Notification actions will not be generated.
default: ""
selector:
text:
debug_enabled:
name: (Optional) Debug
description: >
Enable debugging for automation. If enabled, will send persistent notifications
with extra data.
default: false
selector:
boolean:
mode: single
max_exceeded: silent
variables:
# input vars
input_smart_entity: !input smart_entity
input_objects: !input objects
input_channel: !input channel
input_base_image_url: !input base_image_url
input_base_ha_url: !input base_ha_url
input_lovelace_view: !input lovelace_view
input_debug_enabled: !input debug_enabled
input_notify_target_app: !input notify_target_app
input_notify_target_html5: !input notify_target_html5
input_notify_telegram: !input notify_telegram
input_silence_timer: !input silence_timer
input_send_mobile: !input send_mobile
input_send_html5: !input send_html5
input_send_telegram: !input send_telegram
input_time_format: !input time_format
input_presence_filter: !input presence_filter
# automation data
camera_entities: '[{% for eid in device_entities(device_id(input_smart_entity)) %}{%if eid.startswith(''camera'') and not is_state(eid, ''unavailable'') %}"{{ eid }}",{% endif %}{% endfor %}]'
smart_entities: '[{% for eid in device_entities(device_id(input_smart_entity)) %}{%if eid.startswith(''sensor'') and is_state_attr(eid, "device_class", "occupancy") %}"{{ eid }}",{% endif %}{% endfor %}]'
smart_detect_objs: "{{ (input_objects | lower).split(',') | map('trim') | list | select('in', ['person', 'vehicle']) | list }}"
# automation variables
lovelace_view: "{{ input_lovelace_view | trim }}"
camera_entity_id: "{{ camera_entities | default([None]) | first }}"
trigger_object: "{{ states[input_smart_entity].state }}"
trigger_time: |
{% if states[input_smart_entity] == None %}
None
{% else %}
{{ as_local(states[input_smart_entity].last_changed).strftime(input_time_format) }}
{% endif %}
notification_channel: |
{% if "from_state" in trigger %}
{{ input_channel }}
{% else %}
Manual {{ input_channel }}
{% endif %}
notification_tag: "{{ notification_channel.lower().replace(' ', '-') }}"
notification_title: "{{ device_attr(input_smart_entity, 'name') }}"
notification_url: |
{% if lovelace_view == "" %}
None
{% else %}
{{ input_base_ha_url | trim }}{{ lovelace_view }}
{% endif %}
notification_message: "{{ trigger_object.title() }} detected by {{ notification_title }}{% if trigger_time != None %} at {{ trigger_time }}{% endif %}."
notification_message_html5: |
{{ notification_message }}{% if notification_url != None %}
Tap to open camera in Home Assistant.
{% endif %}
notification_image: |
{% if camera_entity_id == None or input_base_image_url == "" %}
None
{% else %}
{{ input_base_image_url | trim }}{{ state_attr(camera_entity_id, 'entity_picture') }}
{% endif %}
silence_action: "silence-{{ input_smart_entity }}"
trigger:
- platform: state
entity_id: !input smart_entity
from: "none"
condition:
- "{{ not input_presence_filter or not is_state(input_presence_filter, 'home') }}"
- "{{ trigger_object in smart_detect_objs }}"
action:
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: |
Entity ID: `{{ input_smart_entity }}`
Camera: `{{ camera_entity_id }}`
Object: `{{ trigger_object }}`
Notification Service (Mobile): `notify.{{ input_notify_target_app }}`
Notification Service (HTML5): `notify.{{ input_notify_target_html5 }}`
Notification Service (Telegram): `notify.{{ input_notify_telegram }}`
Channel: {{ notification_channel }}
Tag: {{ notification_tag }}
Message: {{ notification_message }}
Image: {{ notification_image }}
URL: {{ notification_url }}
- choose:
- conditions: "{{ input_send_mobile }}"
sequence:
- service: notify.{{ input_notify_target_app }}
data:
message: "{{ notification_message }}"
title: "{{ notification_title }}"
data:
# Android/iOS notification tag
tag: "{{ notification_tag }}"
# Android notification Channel
channel: "{{ notification_channel }}"
# Android high prority
ttl: 0
priority: high
# iOS high prority
time-sensitive: 1
# Android image
image: "{{ notification_image }}"
# iOS image
attachment:
url: "{{ notification_image }}"
actions: >
[{% if notification_url != None %}
{ "action": "URI", "title": "Open Camera", "uri": "{{ lovelace_view }}" },
{% endif %}
{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence", "destructive": True },
{% endif %}]
- choose:
- conditions: "{{ input_send_html5 }}"
sequence:
- service: notify.{{ input_notify_target_html5 }}
data:
message: "{{ notification_message_html5 }}"
title: "{{ notification_title }}"
data:
# HTML5 Notification tag
tag: "{{ notification_tag }}"
image: "{{ notification_image }}"
url: "{{ notification_url }}"
actions: >
[{% if input_silence_timer > 0 %}
{ "action": "{{ silence_action }}", "title": "Silence" },
{% endif %}]
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: notify.{{ input_notify_telegram }}
data:
title: "{{ notification_title }}"
message: "{{ notification_message }}"
data: >
{
{%- if notification_image != None -%}
"photo": {
"url": "{{ notification_image }}",
"caption": "{{ notification_message }}",
"message_tag": "{{ notification_tag }}",
},
{%- endif -%}
{%- if input_silence_timer > 0 -%}
"inline_keyboard": ["Silence:/{{ silence_action }}"],
{%- endif -%}
}
- wait_for_trigger:
- platform: event
event_type: mobile_app_notification_action
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: html5_notification.clicked
event_data:
action: "{{ silence_action }}"
- platform: event
event_type: telegram_callback
event_data:
data: "/{{ silence_action }}"
timeout:
seconds: !input cooldown
continue_on_timeout: false
- choose:
- conditions: "{{ input_send_telegram }}"
sequence:
- service: telegram_bot.answer_callback_query
data:
message: "Smart Detections notifications silenced for {{ input_silence_timer }} minutes"
callback_query_id: "{{ wait.trigger.event.data.id }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence started.
- delay:
minutes: "{{ input_silence_timer }}"
- choose:
- conditions: "{{ input_debug_enabled }}"
sequence:
- service: notify.persistent_notification
data:
title: "Debug: {{ notification_title }}"
message: Silence ended.
================================================
FILE: custom_components/unifiprotect/__init__.py
================================================
"""UniFi Protect Platform."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from aiohttp import CookieJar
from aiohttp.client_exceptions import ServerDisconnectedError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import ModelType
from .const import (
CONF_ALL_UPDATES,
CONF_DOORBELL_TEXT,
CONF_OVERRIDE_CHOST,
CONFIG_OPTIONS,
DEFAULT_SCAN_INTERVAL,
DEVICES_FOR_SUBSCRIBE,
DEVICES_THAT_ADOPT,
DOMAIN,
MIN_REQUIRED_PROTECT_V,
OUTDATED_LOG_MESSAGE,
PLATFORMS,
)
from .data import ProtectData
from .services import async_cleanup_services, async_setup_services
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
@callback
async def _async_migrate_data(
hass: HomeAssistant, entry: ConfigEntry, protect: ProtectApiClient
) -> None:
# already up to date, skip
if CONF_ALL_UPDATES in entry.options:
return
_LOGGER.info("Starting entity migration...")
# migrate entry
options = dict(entry.options)
data = dict(entry.data)
options[CONF_ALL_UPDATES] = False
if CONF_DOORBELL_TEXT in options:
del options[CONF_DOORBELL_TEXT]
hass.config_entries.async_update_entry(entry, data=data, options=options)
# migrate entities
registry = er.async_get(hass)
mac_to_id: dict[str, str] = {}
mac_to_channel_id: dict[str, str] = {}
bootstrap = await protect.get_bootstrap()
for model in DEVICES_THAT_ADOPT:
attr = model.value + "s"
for device in getattr(bootstrap, attr).values():
mac_to_id[device.mac] = device.id
if model != ModelType.CAMERA:
continue
for channel in device.channels:
channel_id = str(channel.id)
if channel.is_rtsp_enabled:
break
mac_to_channel_id[device.mac] = channel_id
count = 0
entities = er.async_entries_for_config_entry(registry, entry.entry_id)
for entity in entities:
new_unique_id: str | None = None
if entity.domain != Platform.CAMERA.value:
parts = entity.unique_id.split("_")
if len(parts) >= 2:
device_or_key = "_".join(parts[:-1])
mac = parts[-1]
device_id = mac_to_id[mac]
if device_or_key == device_id:
new_unique_id = device_id
else:
new_unique_id = f"{device_id}_{device_or_key}"
else:
parts = entity.unique_id.split("_")
if len(parts) == 2:
mac = parts[1]
device_id = mac_to_id[mac]
channel_id = mac_to_channel_id[mac]
new_unique_id = f"{device_id}_{channel_id}"
else:
device_id = parts[0]
channel_id = parts[2]
extra = "" if len(parts) == 3 else "_insecure"
new_unique_id = f"{device_id}_{channel_id}{extra}"
if new_unique_id is None:
continue
_LOGGER.debug(
"Migrating entity %s (old unique_id: %s, new unique_id: %s)",
entity.entity_id,
entity.unique_id,
new_unique_id,
)
try:
registry.async_update_entity(entity.entity_id, new_unique_id=new_unique_id)
except ValueError:
_LOGGER.warning(
"Could not migrate entity %s (old unique_id: %s, new unique_id: %s)",
entity.entity_id,
entity.unique_id,
new_unique_id,
)
else:
count += 1
_LOGGER.info("Migrated %s entities", count)
if count != len(entities):
_LOGGER.warning("%s entities not migrated", len(entities) - count)
@callback
def _async_import_options_from_data_if_missing(
hass: HomeAssistant, entry: ConfigEntry
) -> None:
options = dict(entry.options)
data = dict(entry.data)
modified = False
for importable_option in CONFIG_OPTIONS:
if importable_option not in entry.options and importable_option in entry.data:
options[importable_option] = entry.data[importable_option]
del data[importable_option]
modified = True
if modified:
hass.config_entries.async_update_entry(entry, data=data, options=options)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the UniFi Protect config entries."""
_async_import_options_from_data_if_missing(hass, entry)
session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True))
protect = ProtectApiClient(
host=entry.data[CONF_HOST],
port=entry.data[CONF_PORT],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
verify_ssl=entry.data[CONF_VERIFY_SSL],
session=session,
subscribed_models=DEVICES_FOR_SUBSCRIBE,
override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False),
ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False),
)
_LOGGER.debug("Connect to UniFi Protect")
data_service = ProtectData(hass, protect, SCAN_INTERVAL, entry)
try:
nvr_info = await protect.get_nvr()
except NotAuthorized as err:
raise ConfigEntryAuthFailed(err) from err
except (asyncio.TimeoutError, NvrError, ServerDisconnectedError) as err:
raise ConfigEntryNotReady from err
if nvr_info.version < MIN_REQUIRED_PROTECT_V:
_LOGGER.error(
OUTDATED_LOG_MESSAGE,
nvr_info.version,
MIN_REQUIRED_PROTECT_V,
)
return False
await _async_migrate_data(hass, entry, protect)
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=nvr_info.mac)
await data_service.async_setup()
if not data_service.last_update_success:
raise ConfigEntryNotReady
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_service
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
async_setup_services(hass)
entry.async_on_unload(entry.add_update_listener(_async_options_updated))
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop)
)
return True
async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload UniFi Protect config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
await data.async_stop()
hass.data[DOMAIN].pop(entry.entry_id)
async_cleanup_services(hass)
return bool(unload_ok)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
new = {**config_entry.data}
# keep verify SSL false for anyone migrating to maintain backwards compatibility
new[CONF_VERIFY_SSL] = False
if CONF_DOORBELL_TEXT in new:
del new[CONF_DOORBELL_TEXT]
config_entry.version = 2
hass.config_entries.async_update_entry(config_entry, data=new)
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True
================================================
FILE: custom_components/unifiprotect/binary_sensor.py
================================================
"""This component provides binary sensors for UniFi Protect."""
from __future__ import annotations
from copy import copy
from dataclasses import dataclass
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LAST_TRIP_TIME, ATTR_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data import NVR, Camera, Event, Light, MountType, Sensor
from .const import DOMAIN
from .data import ProtectData
from .entity import (
EventThumbnailMixin,
ProtectDeviceEntity,
ProtectNVREntity,
async_all_device_entities,
)
from .models import ProtectRequiredKeysMixin
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__)
_KEY_DOOR = "door"
@dataclass
class ProtectBinaryEntityDescription(
ProtectRequiredKeysMixin, BinarySensorEntityDescription
):
"""Describes UniFi Protect Binary Sensor entity."""
ufp_last_trip_value: str | None = None
MOUNT_DEVICE_CLASS_MAP = {
MountType.GARAGE: BinarySensorDeviceClass.GARAGE_DOOR,
MountType.WINDOW: BinarySensorDeviceClass.WINDOW,
MountType.DOOR: BinarySensorDeviceClass.DOOR,
}
CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription(
key="doorbell",
name="Doorbell",
device_class=BinarySensorDeviceClass.OCCUPANCY,
icon="mdi:doorbell-video",
ufp_required_field="feature_flags.has_chime",
ufp_value="is_ringing",
ufp_last_trip_value="last_ring",
),
ProtectBinaryEntityDescription(
key="dark",
name="Is Dark",
icon="mdi:brightness-6",
ufp_value="is_dark",
),
)
LIGHT_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription(
key="dark",
name="Is Dark",
icon="mdi:brightness-6",
ufp_value="is_dark",
),
ProtectBinaryEntityDescription(
key="motion",
name="Motion Detected",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_pir_motion_detected",
ufp_last_trip_value="last_motion",
),
)
SENSE_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription(
key=_KEY_DOOR,
name="Contact",
device_class=BinarySensorDeviceClass.DOOR,
ufp_value="is_opened",
ufp_last_trip_value="open_status_changed_at",
ufp_enabled="is_contact_sensor_enabled",
),
ProtectBinaryEntityDescription(
key="battery_low",
name="Battery low",
device_class=BinarySensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
ufp_value="battery_status.is_low",
),
ProtectBinaryEntityDescription(
key="motion",
name="Motion Detected",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_detected",
ufp_last_trip_value="motion_detected_at",
ufp_enabled="is_motion_sensor_enabled",
),
ProtectBinaryEntityDescription(
key="tampering",
name="Tampering Detected",
device_class=BinarySensorDeviceClass.TAMPER,
ufp_value="is_tampering_detected",
ufp_last_trip_value="tampering_detected_at",
),
)
MOTION_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription(
key="motion",
name="Motion",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_detected",
ufp_last_trip_value="last_motion",
),
)
DISK_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
ProtectBinaryEntityDescription(
key="disk_health",
name="Disk {index} Health",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary sensors for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectDeviceBinarySensor,
camera_descs=CAMERA_SENSORS,
light_descs=LIGHT_SENSORS,
sense_descs=SENSE_SENSORS,
)
entities += _async_motion_entities(data)
entities += _async_nvr_entities(data)
async_add_entities(entities)
@callback
def _async_motion_entities(
data: ProtectData,
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
for device in data.api.bootstrap.cameras.values():
for description in MOTION_SENSORS:
entities.append(ProtectEventBinarySensor(data, device, description))
_LOGGER.debug(
"Adding binary sensor entity %s for %s",
description.name,
device.name,
)
return entities
@callback
def _async_nvr_entities(
data: ProtectData,
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
device = data.api.bootstrap.nvr
for index, _ in enumerate(device.system_info.storage.devices):
for description in DISK_SENSORS:
entities.append(
ProtectDiskBinarySensor(data, device, description, index=index)
)
_LOGGER.debug(
"Adding binary sensor entity %s",
(description.name or "{index}").format(index=index),
)
return entities
class ProtectDeviceBinarySensor(ProtectDeviceEntity, BinarySensorEntity):
"""A UniFi Protect Device Binary Sensor."""
device: Camera | Light | Sensor
entity_description: ProtectBinaryEntityDescription
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
if self.entity_description.key == "doorbell":
new_value = self.entity_description.get_ufp_value(self.device)
if new_value != self.is_on:
_LOGGER.debug(
"Changing doorbell sensor from %s to %s", self.is_on, new_value
)
self._attr_is_on = self.entity_description.get_ufp_value(self.device)
if self.entity_description.ufp_last_trip_value is not None:
last_trip = get_nested_attr(
self.device, self.entity_description.ufp_last_trip_value
)
attrs = self.extra_state_attributes or {}
self._attr_extra_state_attributes = {
**attrs,
ATTR_LAST_TRIP_TIME: last_trip,
}
# UP Sense can be any of the 3 contact sensor device classes
if self.entity_description.key == _KEY_DOOR and isinstance(self.device, Sensor):
self.entity_description.device_class = MOUNT_DEVICE_CLASS_MAP.get(
self.device.mount_type, BinarySensorDeviceClass.DOOR
)
class ProtectDiskBinarySensor(ProtectNVREntity, BinarySensorEntity):
"""A UniFi Protect NVR Disk Binary Sensor."""
entity_description: ProtectBinaryEntityDescription
def __init__(
self,
data: ProtectData,
device: NVR,
description: ProtectBinaryEntityDescription,
index: int,
) -> None:
"""Initialize the Binary Sensor."""
description = copy(description)
description.key = f"{description.key}_{index}"
description.name = (description.name or "{index}").format(index=index)
self._index = index
super().__init__(data, device, description)
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
disks = self.device.system_info.storage.devices
disk_available = len(disks) > self._index
self._attr_available = self._attr_available and disk_available
if disk_available:
disk = disks[self._index]
self._attr_is_on = not disk.healthy
self._attr_extra_state_attributes = {ATTR_MODEL: disk.model}
class ProtectEventBinarySensor(EventThumbnailMixin, ProtectDeviceBinarySensor):
"""A UniFi Protect Device Binary Sensor with access tokens."""
device: Camera
@callback
def _async_get_event(self) -> Event | None:
"""Get event from Protect device."""
event: Event | None = None
if self.device.is_motion_detected and self.device.last_motion_event is not None:
event = self.device.last_motion_event
return event
================================================
FILE: custom_components/unifiprotect/button.py
================================================
"""Support for Ubiquiti's UniFi Protect NVR."""
from __future__ import annotations
import logging
from homeassistant.components.button import ButtonDeviceClass, ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
from .const import DEVICES_THAT_ADOPT, DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Discover devices on a UniFi Protect NVR."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
ProtectButton(
data,
device,
)
for device in data.get_by_types(DEVICES_THAT_ADOPT)
]
)
class ProtectButton(ProtectDeviceEntity, ButtonEntity):
"""A Ubiquiti UniFi Protect Reboot button."""
_attr_entity_registry_enabled_default = False
_attr_device_class = ButtonDeviceClass.RESTART
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
) -> None:
"""Initialize an UniFi camera."""
super().__init__(data, device)
self._attr_name = f"{self.device.name} Reboot Device"
async def async_press(self) -> None:
"""Press the button."""
_LOGGER.debug("Rebooting %s with id %s", self.device.model, self.device.id)
await self.device.reboot()
================================================
FILE: custom_components/unifiprotect/camera.py
================================================
"""Support for Ubiquiti's UniFi Protect NVR."""
from __future__ import annotations
from collections.abc import Generator
import logging
from homeassistant.components.camera import SUPPORT_STREAM, Camera
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import Camera as UFPCamera, StateType
from pyunifiprotect.data.devices import CameraChannel
from .const import (
ATTR_BITRATE,
ATTR_CHANNEL_ID,
ATTR_FPS,
ATTR_HEIGHT,
ATTR_WIDTH,
DOMAIN,
)
from .data import ProtectData
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
def get_camera_channels(
protect: ProtectApiClient,
) -> Generator[tuple[UFPCamera, CameraChannel, bool], None, None]:
"""Get all the camera channels."""
for camera in protect.bootstrap.cameras.values():
if not camera.channels:
_LOGGER.warning(
"Camera does not have any channels: %s (id: %s)", camera.name, camera.id
)
continue
is_default = True
for channel in camera.channels:
if channel.is_package:
yield camera, channel, True
elif channel.is_rtsp_enabled:
yield camera, channel, is_default
is_default = False
# no RTSP enabled use first channel with no stream
if is_default:
yield camera, camera.channels[0], True
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Discover cameras on a UniFi Protect NVR."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
disable_stream = data.disable_stream
entities = []
for camera, channel, is_default in get_camera_channels(data.api):
# do not enable streaming for package camera
# 2 FPS causes a lot of buferring
entities.append(
ProtectCamera(
data,
camera,
channel,
is_default,
True,
disable_stream or channel.is_package,
)
)
if channel.is_rtsp_enabled and not channel.is_package:
entities.append(
ProtectCamera(
data,
camera,
channel,
is_default,
False,
disable_stream,
)
)
async_add_entities(entities)
class ProtectCamera(ProtectDeviceEntity, Camera):
"""A Ubiquiti UniFi Protect Camera."""
device: UFPCamera
def __init__(
self,
data: ProtectData,
camera: UFPCamera,
channel: CameraChannel,
is_default: bool,
secure: bool,
disable_stream: bool,
) -> None:
"""Initialize an UniFi camera."""
self.channel = channel
self._secure = secure
self._disable_stream = disable_stream
self._last_image: bytes | None = None
super().__init__(data, camera)
if self._secure:
self._attr_unique_id = f"{self.device.id}_{self.channel.id}"
self._attr_name = f"{self.device.name} {self.channel.name}"
else:
self._attr_unique_id = f"{self.device.id}_{self.channel.id}_insecure"
self._attr_name = f"{self.device.name} {self.channel.name} Insecure"
# only the default (first) channel is enabled by default
self._attr_entity_registry_enabled_default = is_default and secure
@callback
def _async_set_stream_source(self) -> None:
disable_stream = self._disable_stream
if not self.channel.is_rtsp_enabled:
disable_stream = False
rtsp_url = self.channel.rtsp_url
if self._secure:
rtsp_url = self.channel.rtsps_url
# _async_set_stream_source called by __init__
self._stream_source = ( # pylint: disable=attribute-defined-outside-init
None if disable_stream else rtsp_url
)
self._attr_supported_features: int = (
SUPPORT_STREAM if self._stream_source else 0
)
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self.channel = self.device.channels[self.channel.id]
self._attr_motion_detection_enabled = (
self.device.state == StateType.CONNECTED
and self.device.feature_flags.has_motion_zones
)
self._attr_is_recording = (
self.device.state == StateType.CONNECTED and self.device.is_recording
)
self._async_set_stream_source()
self._attr_extra_state_attributes = {
ATTR_WIDTH: self.channel.width,
ATTR_HEIGHT: self.channel.height,
ATTR_FPS: self.channel.fps,
ATTR_BITRATE: self.channel.bitrate,
ATTR_CHANNEL_ID: self.channel.id,
}
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return the Camera Image."""
if self.channel.is_package:
last_image = await self.device.get_package_snapshot(width, height)
else:
last_image = await self.device.get_snapshot(width, height)
self._last_image = last_image
return self._last_image
async def stream_source(self) -> str | None:
"""Return the Stream Source."""
return self._stream_source
================================================
FILE: custom_components/unifiprotect/config_flow.py
================================================
"""Config Flow to configure UniFi Protect Integration."""
from __future__ import annotations
import logging
from typing import Any
from aiohttp import CookieJar
from homeassistant import config_entries
from homeassistant.const import (
CONF_HOST,
CONF_ID,
CONF_PASSWORD,
CONF_PORT,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data.nvr import NVR
import voluptuous as vol
from .const import (
CONF_ALL_UPDATES,
CONF_DISABLE_RTSP,
CONF_OVERRIDE_CHOST,
DEFAULT_PORT,
DEFAULT_VERIFY_SSL,
DOMAIN,
MIN_REQUIRED_PROTECT_V,
OUTDATED_LOG_MESSAGE,
)
_LOGGER = logging.getLogger(__name__)
class ProtectFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a UniFi Protect config flow."""
VERSION = 2
def __init__(self) -> None:
"""Init the config flow."""
super().__init__()
self.entry: config_entries.ConfigEntry | None = None
@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
@callback
def _async_create_entry(self, title: str, data: dict[str, Any]) -> FlowResult:
return self.async_create_entry(
title=title,
data={**data, CONF_ID: title},
options={
CONF_DISABLE_RTSP: False,
CONF_ALL_UPDATES: False,
CONF_OVERRIDE_CHOST: False,
},
)
async def _async_get_nvr_data(
self,
user_input: dict[str, Any],
) -> tuple[NVR | None, dict[str, str]]:
session = async_create_clientsession(
self.hass, cookie_jar=CookieJar(unsafe=True)
)
host = user_input[CONF_HOST]
port = user_input.get(CONF_PORT, DEFAULT_PORT)
verify_ssl = user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
protect = ProtectApiClient(
session=session,
host=host,
port=port,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
verify_ssl=verify_ssl,
)
errors = {}
nvr_data = None
try:
nvr_data = await protect.get_nvr()
except NotAuthorized as ex:
_LOGGER.debug(ex)
errors[CONF_PASSWORD] = "invalid_auth"
except NvrError as ex:
_LOGGER.debug(ex)
errors["base"] = "cannot_connect"
else:
if nvr_data.version < MIN_REQUIRED_PROTECT_V:
_LOGGER.debug(
OUTDATED_LOG_MESSAGE,
nvr_data.version,
MIN_REQUIRED_PROTECT_V,
)
errors["base"] = "protect_version"
return nvr_data, errors
async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult:
"""Perform reauth upon an API authentication error."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm reauth."""
errors: dict[str, str] = {}
assert self.entry is not None
# prepopulate fields
form_data = {**self.entry.data}
if user_input is not None:
form_data.update(user_input)
# validate login data
_, errors = await self._async_get_nvr_data(form_data)
if not errors:
self.hass.config_entries.async_update_entry(self.entry, data=form_data)
await self.hass.config_entries.async_reload(self.entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(
CONF_USERNAME, default=form_data.get(CONF_USERNAME)
): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle a flow initiated by the user."""
errors: dict[str, str] = {}
if user_input is not None:
nvr_data, errors = await self._async_get_nvr_data(user_input)
if nvr_data and not errors:
await self.async_set_unique_id(nvr_data.mac)
self._abort_if_unique_id_configured()
return self._async_create_entry(nvr_data.name, user_input)
user_input = user_input or {}
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str,
vol.Required(
CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT)
): int,
vol.Required(
CONF_VERIFY_SSL,
default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
): bool,
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME)
): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
)
class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(
CONF_DISABLE_RTSP,
default=self.config_entry.options.get(CONF_DISABLE_RTSP, False),
): bool,
vol.Optional(
CONF_ALL_UPDATES,
default=self.config_entry.options.get(CONF_ALL_UPDATES, False),
): bool,
vol.Optional(
CONF_OVERRIDE_CHOST,
default=self.config_entry.options.get(
CONF_OVERRIDE_CHOST, False
),
): bool,
}
),
)
================================================
FILE: custom_components/unifiprotect/const.py
================================================
"""Constant definitions for UniFi Protect Integration."""
from homeassistant.const import Platform
from pyunifiprotect.data.types import ModelType, Version
DOMAIN = "unifiprotect"
ATTR_EVENT_SCORE = "event_score"
ATTR_WIDTH = "width"
ATTR_HEIGHT = "height"
ATTR_FPS = "fps"
ATTR_BITRATE = "bitrate"
ATTR_CHANNEL_ID = "channel_id"
ATTR_MESSAGE = "message"
ATTR_DURATION = "duration"
ATTR_ANONYMIZE = "anonymize"
CONF_DOORBELL_TEXT = "doorbell_text"
CONF_DISABLE_RTSP = "disable_rtsp"
CONF_ALL_UPDATES = "all_updates"
CONF_OVERRIDE_CHOST = "override_connection_host"
CONFIG_OPTIONS = [
CONF_ALL_UPDATES,
CONF_DISABLE_RTSP,
CONF_OVERRIDE_CHOST,
]
DEFAULT_PORT = 443
DEFAULT_ATTRIBUTION = "Powered by UniFi Protect Server"
DEFAULT_BRAND = "Ubiquiti"
DEFAULT_SCAN_INTERVAL = 5
DEFAULT_VERIFY_SSL = False
DEVICES_THAT_ADOPT = {
ModelType.CAMERA,
ModelType.LIGHT,
ModelType.VIEWPORT,
ModelType.SENSOR,
}
DEVICES_WITH_ENTITIES = DEVICES_THAT_ADOPT | {ModelType.NVR}
DEVICES_FOR_SUBSCRIBE = DEVICES_WITH_ENTITIES | {ModelType.EVENT}
MIN_REQUIRED_PROTECT_V = Version("1.20.0")
OUTDATED_LOG_MESSAGE = "You are running v%s of UniFi Protect. Minimum required version is v%s. Please upgrade UniFi Protect and then retry"
TYPE_EMPTY_VALUE = ""
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CAMERA,
Platform.LIGHT,
Platform.MEDIA_PLAYER,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
]
================================================
FILE: custom_components/unifiprotect/data.py
================================================
"""Base class for protect data."""
from __future__ import annotations
from collections.abc import Generator, Iterable
from datetime import timedelta
import logging
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.event import async_track_time_interval
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
from pyunifiprotect.data import (
Bootstrap,
Event,
Liveview,
ModelType,
WSSubscriptionMessage,
)
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel, ProtectDeviceModel
from .const import CONF_DISABLE_RTSP, DEVICES_THAT_ADOPT, DEVICES_WITH_ENTITIES
_LOGGER = logging.getLogger(__name__)
class ProtectData:
"""Coordinate updates."""
def __init__(
self,
hass: HomeAssistant,
protect: ProtectApiClient,
update_interval: timedelta,
entry: ConfigEntry,
) -> None:
"""Initialize an subscriber."""
super().__init__()
self._hass = hass
self._entry = entry
self._hass = hass
self._update_interval = update_interval
self._subscriptions: dict[str, list[CALLBACK_TYPE]] = {}
self._unsub_interval: CALLBACK_TYPE | None = None
self._unsub_websocket: CALLBACK_TYPE | None = None
self.last_update_success = False
self.api = protect
@property
def disable_stream(self) -> bool:
"""Check if RTSP is disabled."""
return self._entry.options.get(CONF_DISABLE_RTSP, False)
def get_by_types(
self, device_types: Iterable[ModelType]
) -> Generator[ProtectAdoptableDeviceModel, None, None]:
"""Get all devices matching types."""
for device_type in device_types:
attr = f"{device_type.value}s"
devices: dict[str, ProtectAdoptableDeviceModel] = getattr(
self.api.bootstrap, attr
)
yield from devices.values()
async def async_setup(self) -> None:
"""Subscribe and do the refresh."""
self._unsub_websocket = self.api.subscribe_websocket(
self._async_process_ws_message
)
await self.async_refresh()
async def async_stop(self, *args: Any) -> None:
"""Stop processing data."""
if self._unsub_websocket:
self._unsub_websocket()
self._unsub_websocket = None
if self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
await self.api.async_disconnect_ws()
async def async_refresh(self, *_: Any, force: bool = False) -> None:
"""Update the data."""
# if last update was failure, force until success
if not self.last_update_success:
force = True
try:
updates = await self.api.update(force=force)
except NvrError:
if self.last_update_success:
_LOGGER.exception("Error while updating")
self.last_update_success = False
# manually trigger update to mark entities unavailable
self._async_process_updates(self.api.bootstrap)
except NotAuthorized:
await self.async_stop()
_LOGGER.exception("Reauthentication required")
self._entry.async_start_reauth(self._hass)
self.last_update_success = False
else:
self.last_update_success = True
self._async_process_updates(updates)
@callback
def _async_process_ws_message(self, message: WSSubscriptionMessage) -> None:
if message.new_obj.model in DEVICES_WITH_ENTITIES:
self.async_signal_device_id_update(message.new_obj.id)
# trigger update for all Cameras with LCD screens when NVR Doorbell settings updates
if "doorbell_settings" in message.changed_data:
_LOGGER.debug(
"Doorbell messages updated. Updating devices with LCD screens"
)
self.api.bootstrap.nvr.update_all_messages()
for camera in self.api.bootstrap.cameras.values():
if camera.feature_flags.has_lcd_screen:
self.async_signal_device_id_update(camera.id)
# trigger updates for camera that the event references
elif isinstance(message.new_obj, Event):
if message.new_obj.camera is not None:
self.async_signal_device_id_update(message.new_obj.camera.id)
elif message.new_obj.light is not None:
self.async_signal_device_id_update(message.new_obj.light.id)
elif message.new_obj.sensor is not None:
self.async_signal_device_id_update(message.new_obj.sensor.id)
# alert user viewport needs restart so voice clients can get new options
elif len(self.api.bootstrap.viewers) > 0 and isinstance(
message.new_obj, Liveview
):
_LOGGER.warning(
"Liveviews updated. Restart Home Assistant to update Viewport select options"
)
@callback
def _async_process_updates(self, updates: Bootstrap | None) -> None:
"""Process update from the protect data."""
# Websocket connected, use data from it
if updates is None:
return
self.async_signal_device_id_update(self.api.bootstrap.nvr.id)
for device_type in DEVICES_THAT_ADOPT:
attr = f"{device_type.value}s"
devices: dict[str, ProtectDeviceModel] = getattr(self.api.bootstrap, attr)
for device_id in devices.keys():
self.async_signal_device_id_update(device_id)
@callback
def async_subscribe_device_id(
self, device_id: str, update_callback: CALLBACK_TYPE
) -> CALLBACK_TYPE:
"""Add an callback subscriber."""
if not self._subscriptions:
self._unsub_interval = async_track_time_interval(
self._hass, self.async_refresh, self._update_interval
)
self._subscriptions.setdefault(device_id, []).append(update_callback)
def _unsubscribe() -> None:
self.async_unsubscribe_device_id(device_id, update_callback)
return _unsubscribe
@callback
def async_unsubscribe_device_id(
self, device_id: str, update_callback: CALLBACK_TYPE
) -> None:
"""Remove a callback subscriber."""
self._subscriptions[device_id].remove(update_callback)
if not self._subscriptions[device_id]:
del self._subscriptions[device_id]
if not self._subscriptions and self._unsub_interval:
self._unsub_interval()
self._unsub_interval = None
@callback
def async_signal_device_id_update(self, device_id: str) -> None:
"""Call the callbacks for a device_id."""
if not self._subscriptions.get(device_id):
return
_LOGGER.debug("Updating device: %s", device_id)
for update_callback in self._subscriptions[device_id]:
update_callback()
================================================
FILE: custom_components/unifiprotect/entity.py
================================================
"""Shared Entity definition for UniFi Protect Integration."""
from __future__ import annotations
from collections.abc import Sequence
import logging
from typing import Any
from homeassistant.core import callback
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
from pyunifiprotect.data import (
Camera,
Event,
Light,
ModelType,
ProtectAdoptableDeviceModel,
Sensor,
StateType,
Viewer,
)
from pyunifiprotect.data.nvr import NVR
from .const import ATTR_EVENT_SCORE, DEFAULT_ATTRIBUTION, DEFAULT_BRAND, DOMAIN
from .data import ProtectData
from .models import ProtectRequiredKeysMixin
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__)
@callback
def _async_device_entities(
data: ProtectData,
klass: type[ProtectDeviceEntity],
model_type: ModelType,
descs: Sequence[ProtectRequiredKeysMixin],
) -> list[ProtectDeviceEntity]:
if len(descs) == 0:
return []
entities: list[ProtectDeviceEntity] = []
for device in data.get_by_types({model_type}):
assert isinstance(device, (Camera, Light, Sensor, Viewer))
for description in descs:
assert isinstance(description, EntityDescription)
if description.ufp_required_field:
required_field = get_nested_attr(device, description.ufp_required_field)
if not required_field:
continue
entities.append(
klass(
data,
device=device,
description=description,
)
)
_LOGGER.debug(
"Adding %s entity %s for %s",
klass.__name__,
description.name,
device.name,
)
return entities
@callback
def async_all_device_entities(
data: ProtectData,
klass: type[ProtectDeviceEntity],
camera_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
light_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
sense_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
viewer_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
all_descs: Sequence[ProtectRequiredKeysMixin] | None = None,
) -> list[ProtectDeviceEntity]:
"""Generate a list of all the device entities."""
all_descs = list(all_descs or [])
camera_descs = list(camera_descs or []) + all_descs
light_descs = list(light_descs or []) + all_descs
sense_descs = list(sense_descs or []) + all_descs
viewer_descs = list(viewer_descs or []) + all_descs
return (
_async_device_entities(data, klass, ModelType.CAMERA, camera_descs)
+ _async_device_entities(data, klass, ModelType.LIGHT, light_descs)
+ _async_device_entities(data, klass, ModelType.SENSOR, sense_descs)
+ _async_device_entities(data, klass, ModelType.VIEWPORT, viewer_descs)
)
class ProtectDeviceEntity(Entity):
"""Base class for UniFi protect entities."""
device: ProtectAdoptableDeviceModel
_attr_should_poll = False
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: EntityDescription | None = None,
) -> None:
"""Initialize the entity."""
super().__init__()
self.data: ProtectData = data
self.device = device
if description is None:
self._attr_unique_id = f"{self.device.id}"
self._attr_name = f"{self.device.name}"
else:
self.entity_description = description
self._attr_unique_id = f"{self.device.id}_{description.key}"
name = description.name or ""
self._attr_name = f"{self.device.name} {name.title()}"
self._attr_attribution = DEFAULT_ATTRIBUTION
self._async_set_device_info()
self._async_update_device_from_protect()
async def async_update(self) -> None:
"""Update the entity.
Only used by the generic entity update service.
"""
await self.data.async_refresh()
@callback
def _async_set_device_info(self) -> None:
self._attr_device_info = DeviceInfo(
name=self.device.name,
manufacturer=DEFAULT_BRAND,
model=self.device.type,
via_device=(DOMAIN, self.data.api.bootstrap.nvr.mac),
sw_version=self.device.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)},
configuration_url=self.device.protect_url,
)
@callback
def _async_update_device_from_protect(self) -> None:
"""Update Entity object from Protect device."""
if self.data.last_update_success:
assert self.device.model
devices = getattr(self.data.api.bootstrap, f"{self.device.model.value}s")
self.device = devices[self.device.id]
is_connected = (
self.data.last_update_success and self.device.state == StateType.CONNECTED
)
if (
hasattr(self, "entity_description")
and self.entity_description is not None
and hasattr(self.entity_description, "get_ufp_enabled")
):
assert isinstance(self.entity_description, ProtectRequiredKeysMixin)
is_connected = is_connected and self.entity_description.get_ufp_enabled(
self.device
)
self._attr_available = is_connected
@callback
def _async_updated_event(self) -> None:
"""Call back for incoming data."""
self._async_update_device_from_protect()
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.data.async_subscribe_device_id(
self.device.id, self._async_updated_event
)
)
class ProtectNVREntity(ProtectDeviceEntity):
"""Base class for unifi protect entities."""
# separate subclass on purpose
device: NVR # type: ignore[assignment]
def __init__(
self,
entry: ProtectData,
device: NVR,
description: EntityDescription | None = None,
) -> None:
"""Initialize the entity."""
super().__init__(entry, device, description) # type: ignore[arg-type]
@callback
def _async_set_device_info(self) -> None:
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, self.device.mac)},
identifiers={(DOMAIN, self.device.mac)},
manufacturer=DEFAULT_BRAND,
name=self.device.name,
model=self.device.type,
sw_version=str(self.device.version),
configuration_url=self.device.api.base_url,
)
@callback
def _async_update_device_from_protect(self) -> None:
if self.data.last_update_success:
self.device = self.data.api.bootstrap.nvr
self._attr_available = self.data.last_update_success
class EventThumbnailMixin(ProtectDeviceEntity):
"""Adds motion event attributes to sensor."""
def __init__(self, *args: Any, **kwarg: Any) -> None:
"""Init an sensor that has event thumbnails."""
super().__init__(*args, **kwarg)
self._event: Event | None = None
@callback
def _async_get_event(self) -> Event | None:
"""Get event from Protect device.
To be overridden by child classes.
"""
raise NotImplementedError()
@callback
def _async_thumbnail_extra_attrs(self) -> dict[str, Any]:
# Camera motion sensors with object detection
attrs: dict[str, Any] = {
ATTR_EVENT_SCORE: 0,
}
if self._event is None:
return attrs
attrs[ATTR_EVENT_SCORE] = self._event.score
return attrs
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._event = self._async_get_event()
attrs = self.extra_state_attributes or {}
self._attr_extra_state_attributes = {
**attrs,
**self._async_thumbnail_extra_attrs(),
}
================================================
FILE: custom_components/unifiprotect/light.py
================================================
"""This component provides Lights for UniFi Protect."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
SUPPORT_BRIGHTNESS,
LightEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data import Light
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up lights for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities = [
ProtectLight(
data,
device,
)
for device in data.api.bootstrap.lights.values()
]
if not entities:
return
async_add_entities(entities)
def unifi_brightness_to_hass(value: int) -> int:
"""Convert unifi brightness 1..6 to hass format 0..255."""
return min(255, round((value / 6) * 255))
def hass_to_unifi_brightness(value: int) -> int:
"""Convert hass brightness 0..255 to unifi 1..6 scale."""
return max(1, round((value / 255) * 6))
class ProtectLight(ProtectDeviceEntity, LightEntity):
"""A Ubiquiti UniFi Protect Light Entity."""
device: Light
_attr_icon = "mdi:spotlight-beam"
_attr_supported_features = SUPPORT_BRIGHTNESS
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_is_on = self.device.is_light_on
self._attr_brightness = unifi_brightness_to_hass(
self.device.light_device_settings.led_level
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on."""
hass_brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
unifi_brightness = hass_to_unifi_brightness(hass_brightness)
_LOGGER.debug("Turning on light with brightness %s", unifi_brightness)
await self.device.set_light(True, unifi_brightness)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""
_LOGGER.debug("Turning off light")
await self.device.set_light(False)
================================================
FILE: custom_components/unifiprotect/manifest.json
================================================
{
"domain": "unifiprotect",
"name": "UniFi Protect",
"documentation": "https://github.com/briis/unifiprotect",
"issue_tracker": "https://github.com/briis/unifiprotect/issues",
"config_flow": true,
"requirements": [
"pyunifiprotect==3.2.1"
],
"dependencies": [
"http"
],
"version": "0.12.0-beta11",
"codeowners": [
"@briis",
"@AngellusMortis",
"@bdraco"
],
"iot_class": "local_push"
}
================================================
FILE: custom_components/unifiprotect/media_player.py
================================================
"""Support for Ubiquiti's UniFi Protect NVR."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityDescription,
)
from homeassistant.components.media_player.const import (
MEDIA_TYPE_MUSIC,
SUPPORT_PLAY_MEDIA,
SUPPORT_STOP,
SUPPORT_VOLUME_SET,
SUPPORT_VOLUME_STEP,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE, STATE_PLAYING
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data import Camera
from pyunifiprotect.exceptions import StreamError
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Discover cameras with speakers on a UniFi Protect NVR."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
ProtectMediaPlayer(
data,
camera,
)
for camera in data.api.bootstrap.cameras.values()
if camera.feature_flags.has_speaker
]
)
class ProtectMediaPlayer(ProtectDeviceEntity, MediaPlayerEntity):
"""A Ubiquiti UniFi Protect Speaker."""
device: Camera
entity_description: MediaPlayerEntityDescription
def __init__(
self,
data: ProtectData,
camera: Camera,
) -> None:
"""Initialize an UniFi speaker."""
super().__init__(
data,
camera,
MediaPlayerEntityDescription(
key="speaker", device_class=MediaPlayerDeviceClass.SPEAKER
),
)
self._attr_name = f"{self.device.name} Speaker"
self._attr_supported_features = (
SUPPORT_PLAY_MEDIA | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_STEP | SUPPORT_STOP
)
self._attr_media_content_type = MEDIA_TYPE_MUSIC
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_volume_level = float(self.device.speaker_settings.volume / 100)
if (
self.device.talkback_stream is not None
and self.device.talkback_stream.is_running
):
self._attr_state = STATE_PLAYING
else:
self._attr_state = STATE_IDLE
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
volume_int = int(volume * 100)
await self.device.set_speaker_volume(volume_int)
async def async_media_stop(self) -> None:
"""Send stop command."""
if (
self.device.talkback_stream is not None
and self.device.talkback_stream.is_running
):
_LOGGER.debug("Stopping playback for %s Speaker", self.device.name)
await self.device.stop_audio()
self._async_updated_event()
async def async_play_media(
self, media_type: str, media_id: str, **kwargs: Any
) -> None:
"""Play a piece of media."""
if media_type != MEDIA_TYPE_MUSIC:
raise ValueError("Only music media type is supported")
_LOGGER.debug("Playing Media %s for %s Speaker", media_id, self.device.name)
await self.async_media_stop()
try:
await self.device.play_audio(media_id, blocking=False)
except StreamError as err:
raise HomeAssistantError from err
else:
# update state after starting player
self._async_updated_event()
# wait until player finishes to update state again
await self.device.wait_until_audio_completes()
self._async_updated_event()
================================================
FILE: custom_components/unifiprotect/models.py
================================================
"""The unifiprotect integration models."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import logging
from typing import Any
from homeassistant.helpers.entity import EntityDescription
from pyunifiprotect.data import NVR, ProtectAdoptableDeviceModel
from .utils import get_nested_attr
_LOGGER = logging.getLogger(__name__)
@dataclass
class ProtectRequiredKeysMixin:
"""Mixin for required keys."""
ufp_required_field: str | None = None
ufp_value: str | None = None
ufp_value_fn: Callable[[ProtectAdoptableDeviceModel | NVR], Any] | None = None
ufp_enabled: str | None = None
def get_ufp_value(self, obj: ProtectAdoptableDeviceModel | NVR) -> Any:
"""Return value from UniFi Protect device."""
if self.ufp_value is not None:
return get_nested_attr(obj, self.ufp_value)
if self.ufp_value_fn is not None:
return self.ufp_value_fn(obj)
# reminder for future that one is required
raise RuntimeError( # pragma: no cover
"`ufp_value` or `ufp_value_fn` is required"
)
def get_ufp_enabled(self, obj: ProtectAdoptableDeviceModel | NVR) -> bool:
"""Return value from UniFi Protect device."""
if self.ufp_enabled is not None:
return bool(get_nested_attr(obj, self.ufp_enabled))
return True
@dataclass
class ProtectSetableKeysMixin(ProtectRequiredKeysMixin):
"""Mixin to for settable values."""
ufp_set_method: str | None = None
ufp_set_method_fn: Callable[
[ProtectAdoptableDeviceModel, Any], Coroutine[Any, Any, None]
] | None = None
async def ufp_set(self, obj: ProtectAdoptableDeviceModel, value: Any) -> None:
"""Set value for UniFi Protect device."""
assert isinstance(self, EntityDescription)
_LOGGER.debug("Setting %s to %s for %s", self.name, value, obj.name)
if self.ufp_set_method is not None:
await getattr(obj, self.ufp_set_method)(value)
elif self.ufp_set_method_fn is not None:
await self.ufp_set_method_fn(obj, value)
================================================
FILE: custom_components/unifiprotect/number.py
================================================
"""This component provides number entities for UniFi Protect."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data.devices import Camera, Light
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin
@dataclass
class NumberKeysMixin:
"""Mixin for required keys."""
ufp_max: int
ufp_min: int
ufp_step: int
@dataclass
class ProtectNumberEntityDescription(
ProtectSetableKeysMixin, NumberEntityDescription, NumberKeysMixin
):
"""Describes UniFi Protect Number entity."""
def _get_pir_duration(obj: Any) -> int:
assert isinstance(obj, Light)
return int(obj.light_device_settings.pir_duration.total_seconds())
async def _set_pir_duration(obj: Any, value: float) -> None:
assert isinstance(obj, Light)
await obj.set_duration(timedelta(seconds=value))
CAMERA_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="wdr_value",
name="Wide Dynamic Range",
icon="mdi:state-machine",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=3,
ufp_step=1,
ufp_required_field="feature_flags.has_wdr",
ufp_value="isp_settings.wdr",
ufp_set_method="set_wdr_level",
),
ProtectNumberEntityDescription(
key="mic_level",
name="Microphone Level",
icon="mdi:microphone",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.has_mic",
ufp_value="mic_volume",
ufp_set_method="set_mic_volume",
),
ProtectNumberEntityDescription(
key="zoom_position",
name="Zoom Level",
icon="mdi:magnify-plus-outline",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field="feature_flags.can_optical_zoom",
ufp_value="isp_settings.zoom_position",
ufp_set_method="set_camera_zoom",
),
)
LIGHT_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="sensitivity",
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="light_device_settings.pir_sensitivity",
ufp_set_method="set_sensitivity",
),
ProtectNumberEntityDescription(
key="duration",
name="Auto-shutoff Duration",
icon="mdi:camera-timer",
entity_category=EntityCategory.CONFIG,
ufp_min=15,
ufp_max=900,
ufp_step=15,
ufp_required_field=None,
ufp_value_fn=_get_pir_duration,
ufp_set_method_fn=_set_pir_duration,
),
)
SENSE_NUMBERS: tuple[ProtectNumberEntityDescription, ...] = (
ProtectNumberEntityDescription(
key="sensitivity",
name="Motion Sensitivity",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_min=0,
ufp_max=100,
ufp_step=1,
ufp_required_field=None,
ufp_value="motion_settings.sensitivity",
ufp_set_method="set_motion_sensitivity",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up number entities for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectNumbers,
camera_descs=CAMERA_NUMBERS,
light_descs=LIGHT_NUMBERS,
sense_descs=SENSE_NUMBERS,
)
async_add_entities(entities)
class ProtectNumbers(ProtectDeviceEntity, NumberEntity):
"""A UniFi Protect Number Entity."""
device: Camera | Light
entity_description: ProtectNumberEntityDescription
def __init__(
self,
data: ProtectData,
device: Camera | Light,
description: ProtectNumberEntityDescription,
) -> None:
"""Initialize the Number Entities."""
super().__init__(data, device, description)
self._attr_max_value = self.entity_description.ufp_max
self._attr_min_value = self.entity_description.ufp_min
self._attr_step = self.entity_description.ufp_step
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_value = self.entity_description.get_ufp_value(self.device)
async def async_set_value(self, value: float) -> None:
"""Set new value."""
await self.entity_description.ufp_set(self.device, value)
================================================
FILE: custom_components/unifiprotect/select.py
================================================
"""This component provides select entities for UniFi Protect."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
import logging
from typing import Any, Final
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity import EntityCategory
from homeassistant.util.dt import utcnow
from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.data import (
Camera,
DoorbellMessageType,
IRLEDMode,
Light,
LightModeEnableType,
LightModeType,
RecordingMode,
Viewer,
)
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
from pyunifiprotect.data.devices import Sensor
from pyunifiprotect.data.nvr import NVR
from pyunifiprotect.data.types import ChimeType, MountType
import voluptuous as vol
from .const import ATTR_DURATION, ATTR_MESSAGE, DOMAIN, TYPE_EMPTY_VALUE
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin
_LOGGER = logging.getLogger(__name__)
_KEY_LIGHT_MOTION = "light_motion"
INFRARED_MODES = [
{"id": IRLEDMode.AUTO.value, "name": "Auto"},
{"id": IRLEDMode.ON.value, "name": "Always Enable"},
{"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"},
{"id": IRLEDMode.OFF.value, "name": "Always Disable"},
]
CHIME_TYPES = [
{"id": ChimeType.NONE.value, "name": "None"},
{"id": ChimeType.MECHANICAL.value, "name": "Mechanical"},
{"id": ChimeType.DIGITAL.value, "name": "Digital"},
]
MOUNT_TYPES = [
{"id": MountType.NONE.value, "name": "None"},
{"id": MountType.DOOR.value, "name": "Door"},
{"id": MountType.WINDOW.value, "name": "Window"},
{"id": MountType.GARAGE.value, "name": "Garage"},
{"id": MountType.LEAK.value, "name": "Leak"},
]
LIGHT_MODE_MOTION = "On Motion - Always"
LIGHT_MODE_MOTION_DARK = "On Motion - When Dark"
LIGHT_MODE_DARK = "When Dark"
LIGHT_MODE_OFF = "Manual"
LIGHT_MODES = [LIGHT_MODE_MOTION, LIGHT_MODE_DARK, LIGHT_MODE_OFF]
LIGHT_MODE_TO_SETTINGS = {
LIGHT_MODE_MOTION: (LightModeType.MOTION.value, LightModeEnableType.ALWAYS.value),
LIGHT_MODE_MOTION_DARK: (
LightModeType.MOTION.value,
LightModeEnableType.DARK.value,
),
LIGHT_MODE_DARK: (LightModeType.WHEN_DARK.value, LightModeEnableType.DARK.value),
LIGHT_MODE_OFF: (LightModeType.MANUAL.value, None),
}
MOTION_MODE_TO_LIGHT_MODE = [
{"id": LightModeType.MOTION.value, "name": LIGHT_MODE_MOTION},
{"id": f"{LightModeType.MOTION.value}Dark", "name": LIGHT_MODE_MOTION_DARK},
{"id": LightModeType.WHEN_DARK.value, "name": LIGHT_MODE_DARK},
{"id": LightModeType.MANUAL.value, "name": LIGHT_MODE_OFF},
]
DEVICE_RECORDING_MODES = [
{"id": mode.value, "name": mode.value.title()} for mode in list(RecordingMode)
]
DEVICE_CLASS_LCD_MESSAGE: Final = "unifiprotect__lcd_message"
SERVICE_SET_DOORBELL_MESSAGE = "set_doorbell_message"
SET_DOORBELL_LCD_MESSAGE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_DURATION, default=""): cv.string,
}
)
@dataclass
class ProtectSelectEntityDescription(ProtectSetableKeysMixin, SelectEntityDescription):
"""Describes UniFi Protect Select entity."""
ufp_options: list[dict[str, Any]] | None = None
ufp_options_callable: Callable[
[ProtectApiClient], list[dict[str, Any]]
] | None = None
ufp_enum_type: type[Enum] | None = None
ufp_set_method: str | None = None
def _get_viewer_options(api: ProtectApiClient) -> list[dict[str, Any]]:
return [
{"id": item.id, "name": item.name} for item in api.bootstrap.liveviews.values()
]
def _get_doorbell_options(api: ProtectApiClient) -> list[dict[str, Any]]:
default_message = api.bootstrap.nvr.doorbell_settings.default_message_text
messages = api.bootstrap.nvr.doorbell_settings.all_messages
built_messages = ({"id": item.type.value, "name": item.text} for item in messages)
return [
{"id": "", "name": f"Default Message ({default_message})"},
*built_messages,
]
def _get_paired_camera_options(api: ProtectApiClient) -> list[dict[str, Any]]:
options = [{"id": TYPE_EMPTY_VALUE, "name": "Not Paired"}]
for camera in api.bootstrap.cameras.values():
options.append({"id": camera.id, "name": camera.name})
return options
def _get_viewer_current(obj: Any) -> str:
assert isinstance(obj, Viewer)
return obj.liveview_id
def _get_light_motion_current(obj: Any) -> str:
assert isinstance(obj, Light)
# a bit of extra to allow On Motion Always/Dark
if (
obj.light_mode_settings.mode == LightModeType.MOTION
and obj.light_mode_settings.enable_at == LightModeEnableType.DARK
):
return f"{LightModeType.MOTION.value}Dark"
return obj.light_mode_settings.mode.value
def _get_doorbell_current(obj: Any) -> str | None:
assert isinstance(obj, Camera)
if obj.lcd_message is None:
return None
return obj.lcd_message.text
async def _set_light_mode(obj: Any, mode: str) -> None:
assert isinstance(obj, Light)
lightmode, timing = LIGHT_MODE_TO_SETTINGS[mode]
await obj.set_light_settings(
LightModeType(lightmode),
enable_at=None if timing is None else LightModeEnableType(timing),
)
async def _set_paired_camera(
obj: ProtectAdoptableDeviceModel | NVR, camera_id: str
) -> None:
assert isinstance(obj, (Sensor, Light))
if camera_id == TYPE_EMPTY_VALUE:
camera: Camera | None = None
else:
camera = obj.api.bootstrap.cameras.get(camera_id)
await obj.set_paired_camera(camera)
async def _set_doorbell_message(obj: Any, message: str) -> None:
assert isinstance(obj, Camera)
if message.startswith(DoorbellMessageType.CUSTOM_MESSAGE.value):
await obj.set_lcd_text(DoorbellMessageType.CUSTOM_MESSAGE, text=message)
elif message == TYPE_EMPTY_VALUE:
await obj.set_lcd_text(None)
else:
await obj.set_lcd_text(DoorbellMessageType(message))
async def _set_liveview(obj: Any, liveview_id: str) -> None:
assert isinstance(obj, Viewer)
liveview = obj.api.bootstrap.liveviews[liveview_id]
await obj.set_liveview(liveview)
CAMERA_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key="recording_mode",
name="Recording Mode",
icon="mdi:video-outline",
entity_category=EntityCategory.CONFIG,
ufp_options=DEVICE_RECORDING_MODES,
ufp_enum_type=RecordingMode,
ufp_value="recording_settings.mode",
ufp_set_method="set_recording_mode",
),
ProtectSelectEntityDescription(
key="infrared",
name="Infrared Mode",
icon="mdi:circle-opacity",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_led_ir",
ufp_options=INFRARED_MODES,
ufp_enum_type=IRLEDMode,
ufp_value="isp_settings.ir_led_mode",
ufp_set_method="set_ir_led_model",
),
ProtectSelectEntityDescription(
key="doorbell_text",
name="Doorbell Text",
icon="mdi:card-text",
entity_category=EntityCategory.CONFIG,
device_class=DEVICE_CLASS_LCD_MESSAGE,
ufp_required_field="feature_flags.has_lcd_screen",
ufp_value_fn=_get_doorbell_current,
ufp_options_callable=_get_doorbell_options,
ufp_set_method_fn=_set_doorbell_message,
),
ProtectSelectEntityDescription(
key="chime_type",
name="Chime Type",
icon="mdi:bell",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_chime",
ufp_options=CHIME_TYPES,
ufp_enum_type=ChimeType,
ufp_value="chime_type",
ufp_set_method="set_chime_type",
),
)
LIGHT_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key=_KEY_LIGHT_MOTION,
name="Light Mode",
icon="mdi:spotlight",
entity_category=EntityCategory.CONFIG,
ufp_options=MOTION_MODE_TO_LIGHT_MODE,
ufp_value_fn=_get_light_motion_current,
ufp_set_method_fn=_set_light_mode,
),
ProtectSelectEntityDescription(
key="paired_camera",
name="Paired Camera",
icon="mdi:cctv",
entity_category=EntityCategory.CONFIG,
ufp_value="camera_id",
ufp_options_callable=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
),
)
SENSE_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key="mount_type",
name="Mount Type",
icon="mdi:screwdriver",
entity_category=EntityCategory.CONFIG,
ufp_options=MOUNT_TYPES,
ufp_enum_type=MountType,
ufp_value="mount_type",
ufp_set_method="set_mount_type",
),
ProtectSelectEntityDescription(
key="paired_camera",
name="Paired Camera",
icon="mdi:cctv",
entity_category=EntityCategory.CONFIG,
ufp_value="camera_id",
ufp_options_callable=_get_paired_camera_options,
ufp_set_method_fn=_set_paired_camera,
),
)
VIEWER_SELECTS: tuple[ProtectSelectEntityDescription, ...] = (
ProtectSelectEntityDescription(
key="viewer",
name="Liveview",
icon="mdi:view-dashboard",
entity_category=None,
ufp_options_callable=_get_viewer_options,
ufp_value_fn=_get_viewer_current,
ufp_set_method_fn=_set_liveview,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: entity_platform.AddEntitiesCallback,
) -> None:
"""Set up number entities for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectSelects,
camera_descs=CAMERA_SELECTS,
light_descs=LIGHT_SELECTS,
sense_descs=SENSE_SELECTS,
viewer_descs=VIEWER_SELECTS,
)
async_add_entities(entities)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_DOORBELL_MESSAGE,
SET_DOORBELL_LCD_MESSAGE_SCHEMA,
"async_set_doorbell_message",
)
class ProtectSelects(ProtectDeviceEntity, SelectEntity):
"""A UniFi Protect Select Entity."""
device: Camera | Light | Viewer
entity_description: ProtectSelectEntityDescription
def __init__(
self,
data: ProtectData,
device: Camera | Light | Viewer,
description: ProtectSelectEntityDescription,
) -> None:
"""Initialize the unifi protect select entity."""
super().__init__(data, device, description)
self._attr_name = f"{self.device.name} {self.entity_description.name}"
self._async_set_options()
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
# entities with categories are not exposed for voice and safe to update dynamically
if (
self.entity_description.entity_category is not None
and self.entity_description.ufp_options_callable is not None
):
_LOGGER.debug(
"Updating dynamic select options for %s", self.entity_description.name
)
self._async_set_options()
@callback
def _async_set_options(self) -> None:
"""Set options attributes from UniFi Protect device."""
if self.entity_description.ufp_options is not None:
options = self.entity_description.ufp_options
else:
assert self.entity_description.ufp_options_callable is not None
options = self.entity_description.ufp_options_callable(self.data.api)
self._attr_options = [item["name"] for item in options]
self._hass_to_unifi_options = {item["name"]: item["id"] for item in options}
self._unifi_to_hass_options = {item["id"]: item["name"] for item in options}
@property
def current_option(self) -> str:
"""Return the current selected option."""
unifi_value = self.entity_description.get_ufp_value(self.device)
if unifi_value is None:
unifi_value = TYPE_EMPTY_VALUE
return self._unifi_to_hass_options.get(unifi_value, unifi_value)
async def async_select_option(self, option: str) -> None:
"""Change the Select Entity Option."""
# Light Motion is a bit different
if self.entity_description.key == _KEY_LIGHT_MOTION:
assert self.entity_description.ufp_set_method_fn is not None
await self.entity_description.ufp_set_method_fn(self.device, option)
return
unifi_value = self._hass_to_unifi_options[option]
if self.entity_description.ufp_enum_type is not None:
unifi_value = self.entity_description.ufp_enum_type(unifi_value)
await self.entity_description.ufp_set(self.device, unifi_value)
async def async_set_doorbell_message(self, message: str, duration: str) -> None:
"""Set LCD Message on Doorbell display."""
if self.entity_description.device_class != DEVICE_CLASS_LCD_MESSAGE:
raise HomeAssistantError("Not a doorbell text select entity")
assert isinstance(self.device, Camera)
reset_at = None
timeout_msg = ""
if duration.isnumeric():
reset_at = utcnow() + timedelta(minutes=int(duration))
timeout_msg = f" with timeout of {duration} minute(s)"
_LOGGER.debug(
'Setting message for %s to "%s"%s', self.device.name, message, timeout_msg
)
await self.device.set_lcd_text(
DoorbellMessageType.CUSTOM_MESSAGE, message, reset_at=reset_at
)
================================================
FILE: custom_components/unifiprotect/sensor.py
================================================
"""This component provides sensors for UniFi Protect."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
import logging
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DATA_BYTES,
DATA_RATE_BYTES_PER_SECOND,
DATA_RATE_MEGABITS_PER_SECOND,
ELECTRIC_POTENTIAL_VOLT,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
TEMP_CELSIUS,
TIME_SECONDS,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data import NVR, Camera, Event
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
from pyunifiprotect.data.devices import Sensor
from .const import DOMAIN
from .data import ProtectData
from .entity import (
EventThumbnailMixin,
ProtectDeviceEntity,
ProtectNVREntity,
async_all_device_entities,
)
from .models import ProtectRequiredKeysMixin
_LOGGER = logging.getLogger(__name__)
OBJECT_TYPE_NONE = "none"
DEVICE_CLASS_DETECTION = "unifiprotect__detection"
@dataclass
class ProtectSensorEntityDescription(ProtectRequiredKeysMixin, SensorEntityDescription):
"""Describes UniFi Protect Sensor entity."""
precision: int | None = None
def get_ufp_value(self, obj: ProtectAdoptableDeviceModel | NVR) -> Any:
"""Return value from UniFi Protect device."""
value = super().get_ufp_value(obj)
if isinstance(value, float) and self.precision:
value = round(value, self.precision)
return value
def _get_uptime(obj: ProtectAdoptableDeviceModel | NVR) -> datetime | None:
if obj.up_since is None:
return None
# up_since can vary slightly over time
# truncate to ensure no extra state_change events fire
return obj.up_since.replace(second=0, microsecond=0)
def _get_nvr_recording_capacity(obj: Any) -> int:
assert isinstance(obj, NVR)
if obj.storage_stats.capacity is None:
return 0
return int(obj.storage_stats.capacity.total_seconds())
def _get_nvr_memory(obj: Any) -> float | None:
assert isinstance(obj, NVR)
memory = obj.system_info.memory
if memory.available is None or memory.total is None:
return None
return (1 - memory.available / memory.total) * 100
def _get_alarm_sound(obj: ProtectAdoptableDeviceModel | NVR) -> str:
assert isinstance(obj, Sensor)
alarm_type = OBJECT_TYPE_NONE
if (
obj.is_alarm_detected
and obj.last_alarm_event is not None
and obj.last_alarm_event.metadata is not None
):
alarm_type = obj.last_alarm_event.metadata.alarm_type or OBJECT_TYPE_NONE
return alarm_type.lower()
ALL_DEVICES_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="uptime",
name="Uptime",
icon="mdi:clock",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
ufp_value_fn=_get_uptime,
),
ProtectSensorEntityDescription(
key="ble_signal",
name="Bluetooth Signal Strength",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="bluetooth_connection_state.signal_strength",
ufp_required_field="bluetooth_connection_state.signal_strength",
),
ProtectSensorEntityDescription(
key="phy_rate",
name="Link Speed",
native_unit_of_measurement=DATA_RATE_MEGABITS_PER_SECOND,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="wired_connection_state.phy_rate",
ufp_required_field="wired_connection_state.phy_rate",
),
ProtectSensorEntityDescription(
key="wifi_signal",
name="WiFi Signal Strength",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="wifi_connection_state.signal_strength",
ufp_required_field="wifi_connection_state.signal_strength",
),
)
CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="oldest_recording",
name="Oldest Recording",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
ufp_value="stats.video.recording_start",
),
ProtectSensorEntityDescription(
key="storage_used",
name="Storage Used",
native_unit_of_measurement=DATA_BYTES,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="stats.storage.used",
),
ProtectSensorEntityDescription(
key="write_rate",
name="Disk Write Rate",
native_unit_of_measurement=DATA_RATE_BYTES_PER_SECOND,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="stats.storage.rate",
precision=2,
),
ProtectSensorEntityDescription(
key="voltage",
name="Voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="voltage",
# no feature flag, but voltage will be null if device does not have voltage sensor
# (i.e. is not G4 Doorbell or not on 1.20.1+)
ufp_required_field="voltage",
precision=2,
),
)
CAMERA_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="stats_rx",
name="Received Data",
native_unit_of_measurement=DATA_BYTES,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
ufp_value="stats.rx_bytes",
),
ProtectSensorEntityDescription(
key="stats_tx",
name="Transferred Data",
native_unit_of_measurement=DATA_BYTES,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL_INCREASING,
ufp_value="stats.tx_bytes",
),
)
SENSE_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="battery_level",
name="Battery Level",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="battery_status.percentage",
),
ProtectSensorEntityDescription(
key="light_level",
name="Light Level",
native_unit_of_measurement=LIGHT_LUX,
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="stats.light.value",
ufp_enabled="is_light_sensor_enabled",
),
ProtectSensorEntityDescription(
key="humidity_level",
name="Humidity Level",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="stats.humidity.value",
ufp_enabled="is_humidity_sensor_enabled",
),
ProtectSensorEntityDescription(
key="temperature_level",
name="Temperature",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="stats.temperature.value",
ufp_enabled="is_temperature_sensor_enabled",
),
ProtectSensorEntityDescription(
key="alarm_sound",
name="Alarm Sound Detected",
ufp_value_fn=_get_alarm_sound,
ufp_enabled="is_alarm_sensor_enabled",
),
)
NVR_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="uptime",
name="Uptime",
icon="mdi:clock",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
ufp_value_fn=_get_uptime,
),
ProtectSensorEntityDescription(
key="storage_utilization",
name="Storage Utilization",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:harddisk",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.utilization",
precision=2,
),
ProtectSensorEntityDescription(
key="record_rotating",
name="Type: Timelapse Video",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:server",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.timelapse_recordings.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="record_timelapse",
name="Type: Continuous Video",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:server",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.continuous_recordings.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="record_detections",
name="Type: Detections Video",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:server",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.detections_recordings.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="resolution_HD",
name="Resolution: HD Video",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:cctv",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.hd_usage.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="resolution_4K",
name="Resolution: 4K Video",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:cctv",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.uhd_usage.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="resolution_free",
name="Resolution: Free Space",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:cctv",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="storage_stats.storage_distribution.free.percentage",
precision=2,
),
ProtectSensorEntityDescription(
key="record_capacity",
name="Recording Capacity",
native_unit_of_measurement=TIME_SECONDS,
icon="mdi:record-rec",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value_fn=_get_nvr_recording_capacity,
),
)
NVR_DISABLED_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="cpu_utilization",
name="CPU Utilization",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:speedometer",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="system_info.cpu.average_load",
),
ProtectSensorEntityDescription(
key="cpu_temperature",
name="CPU Temperature",
native_unit_of_measurement=TEMP_CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value="system_info.cpu.temperature",
),
ProtectSensorEntityDescription(
key="memory_utilization",
name="Memory Utilization",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:memory",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
ufp_value_fn=_get_nvr_memory,
precision=2,
),
)
MOTION_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
ProtectSensorEntityDescription(
key="detected_object",
name="Detected Object",
device_class=DEVICE_CLASS_DETECTION,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectDeviceSensor,
all_descs=ALL_DEVICES_SENSORS,
camera_descs=CAMERA_SENSORS + CAMERA_DISABLED_SENSORS,
sense_descs=SENSE_SENSORS,
)
entities += _async_motion_entities(data)
entities += _async_nvr_entities(data)
async_add_entities(entities)
@callback
def _async_motion_entities(
data: ProtectData,
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
for device in data.api.bootstrap.cameras.values():
if not device.feature_flags.has_smart_detect:
continue
for description in MOTION_SENSORS:
entities.append(ProtectEventSensor(data, device, description))
_LOGGER.debug(
"Adding sensor entity %s for %s",
description.name,
device.name,
)
return entities
@callback
def _async_nvr_entities(
data: ProtectData,
) -> list[ProtectDeviceEntity]:
entities: list[ProtectDeviceEntity] = []
device = data.api.bootstrap.nvr
for description in NVR_SENSORS + NVR_DISABLED_SENSORS:
entities.append(ProtectNVRSensor(data, device, description))
_LOGGER.debug("Adding NVR sensor entity %s", description.name)
return entities
class ProtectDeviceSensor(ProtectDeviceEntity, SensorEntity):
"""A Ubiquiti UniFi Protect Sensor."""
entity_description: ProtectSensorEntityDescription
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: ProtectSensorEntityDescription,
) -> None:
"""Initialize an UniFi Protect sensor."""
super().__init__(data, device, description)
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
class ProtectNVRSensor(ProtectNVREntity, SensorEntity):
"""A Ubiquiti UniFi Protect Sensor."""
entity_description: ProtectSensorEntityDescription
def __init__(
self,
data: ProtectData,
device: NVR,
description: ProtectSensorEntityDescription,
) -> None:
"""Initialize an UniFi Protect sensor."""
super().__init__(data, device, description)
@callback
def _async_update_device_from_protect(self) -> None:
super()._async_update_device_from_protect()
self._attr_native_value = self.entity_description.get_ufp_value(self.device)
class ProtectEventSensor(ProtectDeviceSensor, EventThumbnailMixin):
"""A UniFi Protect Device Sensor with access tokens."""
device: Camera
@callback
def _async_get_event(self) -> Event | None:
"""Get event from Protect device."""
event: Event | None = None
if (
self.device.is_smart_detected
and self.device.last_smart_detect_event is not None
and len(self.device.last_smart_detect_event.smart_detect_types) > 0
):
event = self.device.last_smart_detect_event
return event
@callback
def _async_update_device_from_protect(self) -> None:
# do not call ProtectDeviceSensor method since we want event to get value here
EventThumbnailMixin._async_update_device_from_protect(self)
if self._event is None:
self._attr_native_value = OBJECT_TYPE_NONE
else:
self._attr_native_value = self._event.smart_detect_types[0].value
================================================
FILE: custom_components/unifiprotect/services.py
================================================
"""UniFi Protect Integration services."""
from __future__ import annotations
import asyncio
import functools
from typing import Any
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.service import async_extract_referenced_entity_ids
from pydantic import ValidationError
from pyunifiprotect.api import ProtectApiClient
from pyunifiprotect.exceptions import BadRequest
import voluptuous as vol
from .const import ATTR_MESSAGE, DOMAIN
from .data import ProtectData
SERVICE_ADD_DOORBELL_TEXT = "add_doorbell_text"
SERVICE_REMOVE_DOORBELL_TEXT = "remove_doorbell_text"
SERVICE_SET_DEFAULT_DOORBELL_TEXT = "set_default_doorbell_text"
ALL_GLOBAL_SERIVCES = [
SERVICE_ADD_DOORBELL_TEXT,
SERVICE_REMOVE_DOORBELL_TEXT,
SERVICE_SET_DEFAULT_DOORBELL_TEXT,
]
DOORBELL_TEXT_SCHEMA = vol.All(
vol.Schema(
{
**cv.ENTITY_SERVICE_FIELDS,
vol.Required(ATTR_MESSAGE): cv.string,
},
),
cv.has_at_least_one_key(ATTR_DEVICE_ID),
)
def _async_all_ufp_instances(hass: HomeAssistant) -> list[ProtectApiClient]:
"""All active UFP instances."""
return [
data.api for data in hass.data[DOMAIN].values() if isinstance(data, ProtectData)
]
@callback
def _async_unifi_mac_from_hass(mac: str) -> str:
# MAC addresses in UFP are always caps
return mac.replace(":", "").upper()
@callback
def _async_get_macs_for_device(device_entry: dr.DeviceEntry) -> list[str]:
return [
_async_unifi_mac_from_hass(cval)
for ctype, cval in device_entry.connections
if ctype == dr.CONNECTION_NETWORK_MAC
]
@callback
def _async_get_ufp_instances(
hass: HomeAssistant, device_id: str
) -> tuple[dr.DeviceEntry, ProtectApiClient]:
device_registry = dr.async_get(hass)
if not (device_entry := device_registry.async_get(device_id)):
raise HomeAssistantError(f"No device found for device id: {device_id}")
if device_entry.via_device_id is not None:
return _async_get_ufp_instances(hass, device_entry.via_device_id)
macs = _async_get_macs_for_device(device_entry)
ufp_instances = [
i for i in _async_all_ufp_instances(hass) if i.bootstrap.nvr.mac in macs
]
if not ufp_instances:
# should not be possible unless user manually enters a bad device ID
raise HomeAssistantError( # pragma: no cover
f"No UniFi Protect NVR found for device ID: {device_id}"
)
return device_entry, ufp_instances[0]
@callback
def _async_get_protect_from_call(
hass: HomeAssistant, call: ServiceCall
) -> list[tuple[dr.DeviceEntry, ProtectApiClient]]:
referenced = async_extract_referenced_entity_ids(hass, call)
instances: list[tuple[dr.DeviceEntry, ProtectApiClient]] = []
for device_id in referenced.referenced_devices:
instances.append(_async_get_ufp_instances(hass, device_id))
return instances
async def _async_call_nvr(
instances: list[tuple[dr.DeviceEntry, ProtectApiClient]],
method: str,
*args: Any,
**kwargs: Any,
) -> None:
try:
await asyncio.gather(
*(getattr(i.bootstrap.nvr, method)(*args, **kwargs) for _, i in instances)
)
except (BadRequest, ValidationError) as err:
raise HomeAssistantError(str(err)) from err
async def add_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None:
"""Add a custom doorbell text message."""
message: str = call.data[ATTR_MESSAGE]
instances = _async_get_protect_from_call(hass, call)
await _async_call_nvr(instances, "add_custom_doorbell_message", message)
async def remove_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None:
"""Remove a custom doorbell text message."""
message: str = call.data[ATTR_MESSAGE]
instances = _async_get_protect_from_call(hass, call)
await _async_call_nvr(instances, "remove_custom_doorbell_message", message)
async def set_default_doorbell_text(hass: HomeAssistant, call: ServiceCall) -> None:
"""Set the default doorbell text message."""
message: str = call.data[ATTR_MESSAGE]
instances = _async_get_protect_from_call(hass, call)
await _async_call_nvr(instances, "set_default_doorbell_message", message)
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up the global UniFi Protect services."""
services = [
(
SERVICE_ADD_DOORBELL_TEXT,
functools.partial(add_doorbell_text, hass),
DOORBELL_TEXT_SCHEMA,
),
(
SERVICE_REMOVE_DOORBELL_TEXT,
functools.partial(remove_doorbell_text, hass),
DOORBELL_TEXT_SCHEMA,
),
(
SERVICE_SET_DEFAULT_DOORBELL_TEXT,
functools.partial(set_default_doorbell_text, hass),
DOORBELL_TEXT_SCHEMA,
),
]
for name, method, schema in services:
if hass.services.has_service(DOMAIN, name):
continue
hass.services.async_register(DOMAIN, name, method, schema=schema)
def async_cleanup_services(hass: HomeAssistant) -> None:
"""Cleanup global UniFi Protect services (if all config entries unloaded)."""
loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
if len(loaded_entries) == 1:
for name in ALL_GLOBAL_SERIVCES:
hass.services.async_remove(DOMAIN, name)
================================================
FILE: custom_components/unifiprotect/services.yaml
================================================
add_doorbell_text:
name: Add Custom Doorbell Text
description: Adds a new custom message for Doorbells.
fields:
device_id:
name: UniFi Protect NVR
description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances.
required: true
selector:
device:
integration: unifiprotect
message:
name: Custom Message
description: New custom message to add for Doorbells. Must be less than 30 characters.
example: Come In
required: true
selector:
text:
remove_doorbell_text:
name: Remove Custom Doorbell Text
description: Removes an existing message for Doorbells.
fields:
device_id:
name: UniFi Protect NVR
description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances.
required: true
selector:
device:
integration: unifiprotect
message:
name: Custom Message
description: Existing custom message to remove for Doorbells.
example: Go Away!
required: true
selector:
text:
set_default_doorbell_text:
name: Set Default Doorbell Text
description: Sets the default doorbell message. This will be the message that is automatically selected when a message "expires".
fields:
device_id:
name: UniFi Protect NVR
description: Any device from the UniFi Protect instance you want to change. In case you have multiple Protect Instances.
required: true
selector:
device:
integration: unifiprotect
message:
name: Default Message
description: The default message for your Doorbell. Must be less than 30 characters.
example: Welcome!
required: true
selector:
text:
set_doorbell_message:
name: Set Doorbell message
description: >
Use to dynamically set the message on a Doorbell LCD screen. This service should only be used to set dynamic messages (i.e. setting the current outdoor temperature on your Doorbell). Static messages should still be set using the Select entity and can be added/removed using the add_doorbell_text/remove_doorbell_text services.
fields:
entity_id:
name: Doorbell Text
description: The Doorbell Text select entity for your Doorbell.
example: "select.front_doorbell_camera_doorbell_text"
required: true
selector:
entity:
integration: unifiprotect
domain: select
message:
name: Message to display
description: The message you would like to display on the LCD screen of your Doorbell. Must be less than 30 characters.
example: "Welcome | 09:23 | 25°C"
required: true
selector:
text:
duration:
name: Duration
description: Number of minutes to display the message for before returning to the default message. The default is to not expire.
example: 5
selector:
number:
min: 1
max: 120
step: 1
mode: slider
unit_of_measurement: minutes
================================================
FILE: custom_components/unifiprotect/strings.json
================================================
{
"config": {
"abort": {
"server_exists": "UniFi Protect Server already configured.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"protect_version": "UniFi Protect Version is not supported by this Integration",
"nvr_error": "Error retrieving data from UniFi Protect."
},
"step": {
"user": {
"title": "UniFi Protect Setup",
"description": "Set up UniFi Protect to monitor cameras and sensors.",
"data": {
"host": "IP/Host of UniFi Protect Server",
"port": "[%key:common::config_flow::data::port%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"title": "UniFi Protect Reauth",
"data": {
"host": "IP/Host of UniFi Protect Server",
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
}
},
"options": {
"step": {
"init": {
"title": "UniFi Protect Options",
"description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.",
"data": {
"disable_rtsp": "Disable the RTSP stream",
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
"override_connection_host": "Override Connection Host"
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/switch.py
================================================
"""This component provides Switches for UniFi Protect."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from pyunifiprotect.data import Camera, RecordingMode, VideoMode
from pyunifiprotect.data.base import ProtectAdoptableDeviceModel
from .const import DOMAIN
from .data import ProtectData
from .entity import ProtectDeviceEntity, async_all_device_entities
from .models import ProtectSetableKeysMixin
_LOGGER = logging.getLogger(__name__)
@dataclass
class ProtectSwitchEntityDescription(ProtectSetableKeysMixin, SwitchEntityDescription):
"""Describes UniFi Protect Switch entity."""
_KEY_PRIVACY_MODE = "privacy_mode"
def _get_is_highfps(obj: Any) -> bool:
assert isinstance(obj, Camera)
return bool(obj.video_mode == VideoMode.HIGH_FPS)
async def _set_highfps(obj: Any, value: bool) -> None:
assert isinstance(obj, Camera)
if value:
await obj.set_video_mode(VideoMode.HIGH_FPS)
else:
await obj.set_video_mode(VideoMode.DEFAULT)
ALL_DEVICES_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="ssh",
name="SSH Enabled",
icon="mdi:lock",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
ufp_value="is_ssh_enabled",
ufp_set_method="set_ssh",
),
)
CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_led_status",
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
),
ProtectSwitchEntityDescription(
key="hdr_mode",
name="HDR Mode",
icon="mdi:brightness-7",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_hdr",
ufp_value="hdr_mode",
ufp_set_method="set_hdr",
),
ProtectSwitchEntityDescription(
key="high_fps",
name="High FPS",
icon="mdi:video-high-definition",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_highfps",
ufp_value_fn=_get_is_highfps,
ufp_set_method_fn=_set_highfps,
),
ProtectSwitchEntityDescription(
key=_KEY_PRIVACY_MODE,
name="Privacy Mode",
icon="mdi:eye-settings",
entity_category=None,
ufp_required_field="feature_flags.has_privacy_mask",
ufp_value="is_privacy_on",
),
ProtectSwitchEntityDescription(
key="system_sounds",
name="System Sounds",
icon="mdi:speaker",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_speaker",
ufp_value="speaker_settings.are_system_sounds_enabled",
ufp_set_method="set_system_sounds",
),
ProtectSwitchEntityDescription(
key="osd_name",
name="Overlay: Show Name",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_name_enabled",
ufp_set_method="set_osd_name",
),
ProtectSwitchEntityDescription(
key="osd_date",
name="Overlay: Show Date",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_date_enabled",
ufp_set_method="set_osd_date",
),
ProtectSwitchEntityDescription(
key="osd_logo",
name="Overlay: Show Logo",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_logo_enabled",
ufp_set_method="set_osd_logo",
),
ProtectSwitchEntityDescription(
key="osd_bitrate",
name="Overlay: Show Bitrate",
icon="mdi:fullscreen",
entity_category=EntityCategory.CONFIG,
ufp_value="osd_settings.is_debug_enabled",
ufp_set_method="set_osd_bitrate",
),
ProtectSwitchEntityDescription(
key="smart_person",
name="Detections: Person",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_smart_detect",
ufp_value="is_person_detection_on",
ufp_set_method="set_person_detection",
),
ProtectSwitchEntityDescription(
key="smart_vehicle",
name="Detections: Vehicle",
icon="mdi:car",
entity_category=EntityCategory.CONFIG,
ufp_required_field="feature_flags.has_smart_detect",
ufp_value="is_vehicle_detection_on",
ufp_set_method="set_vehicle_detection",
),
)
SENSE_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_value="led_settings.is_enabled",
ufp_set_method="set_status_light",
),
ProtectSwitchEntityDescription(
key="motion",
name="Motion Detection",
icon="mdi:walk",
entity_category=EntityCategory.CONFIG,
ufp_value="motion_settings.is_enabled",
ufp_set_method="set_motion_status",
),
ProtectSwitchEntityDescription(
key="temperature",
name="Temperature Sensor",
icon="mdi:thermometer",
entity_category=EntityCategory.CONFIG,
ufp_value="temperature_settings.is_enabled",
ufp_set_method="set_temperature_status",
),
ProtectSwitchEntityDescription(
key="humidity",
name="Humidity Sensor",
icon="mdi:water-percent",
entity_category=EntityCategory.CONFIG,
ufp_value="humidity_settings.is_enabled",
ufp_set_method="set_humidity_status",
),
ProtectSwitchEntityDescription(
key="light",
name="Light Sensor",
icon="mdi:brightness-5",
entity_category=EntityCategory.CONFIG,
ufp_value="light_settings.is_enabled",
ufp_set_method="set_light_status",
),
ProtectSwitchEntityDescription(
key="alarm",
name="Alarm Sound Detection",
entity_category=EntityCategory.CONFIG,
ufp_value="alarm_settings.is_enabled",
ufp_set_method="set_alarm_status",
),
)
LIGHT_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
ProtectSwitchEntityDescription(
key="status_light",
name="Status Light On",
icon="mdi:led-on",
entity_category=EntityCategory.CONFIG,
ufp_value="light_device_settings.is_indicator_enabled",
ufp_set_method="set_status_light",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensors for UniFi Protect integration."""
data: ProtectData = hass.data[DOMAIN][entry.entry_id]
entities: list[ProtectDeviceEntity] = async_all_device_entities(
data,
ProtectSwitch,
all_descs=ALL_DEVICES_SWITCHES,
camera_descs=CAMERA_SWITCHES,
light_descs=LIGHT_SWITCHES,
sense_descs=SENSE_SWITCHES,
)
async_add_entities(entities)
class ProtectSwitch(ProtectDeviceEntity, SwitchEntity):
"""A UniFi Protect Switch."""
entity_description: ProtectSwitchEntityDescription
def __init__(
self,
data: ProtectData,
device: ProtectAdoptableDeviceModel,
description: ProtectSwitchEntityDescription,
) -> None:
"""Initialize an UniFi Protect Switch."""
super().__init__(data, device, description)
self._attr_name = f"{self.device.name} {self.entity_description.name}"
self._switch_type = self.entity_description.key
if not isinstance(self.device, Camera):
return
if self.entity_description.key == _KEY_PRIVACY_MODE:
if self.device.is_privacy_on:
self._previous_mic_level = 100
self._previous_record_mode = RecordingMode.ALWAYS
else:
self._previous_mic_level = self.device.mic_volume
self._previous_record_mode = self.device.recording_settings.mode
@property
def is_on(self) -> bool:
"""Return true if device is on."""
return self.entity_description.get_ufp_value(self.device) is True
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if self._switch_type == _KEY_PRIVACY_MODE:
assert isinstance(self.device, Camera)
self._previous_mic_level = self.device.mic_volume
self._previous_record_mode = self.device.recording_settings.mode
await self.device.set_privacy(True, 0, RecordingMode.NEVER)
else:
await self.entity_description.ufp_set(self.device, True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if self._switch_type == _KEY_PRIVACY_MODE:
assert isinstance(self.device, Camera)
_LOGGER.debug("Setting Privacy Mode to false for %s", self.device.name)
await self.device.set_privacy(
False, self._previous_mic_level, self._previous_record_mode
)
else:
await self.entity_description.ufp_set(self.device, False)
================================================
FILE: custom_components/unifiprotect/translations/da.json
================================================
{
"config": {
"abort": {
"server_exists": "Denne UniFi Protect Server er allerede konfigureret."
},
"error": {
"connection_error": "Kan ikke forbinde. Forkert IP eller Brugernavn/Kodeord.",
"protect_version": "UniFi Protect Version er ikke understøttet af denne version.",
"nvr_error": "Kan ikke hente data fra UniFi Protect."
},
"step": {
"user": {
"description": "Konfigurer UniFi Protect for at monitorere kameraer og sensorer.",
"title": "UniFi Protect",
"data": {
"host": "IP adresse på UniFi Protect Server",
"port": "Port nummer",
"username": "Brugernavn",
"password": "Kodeord"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"username": "Brugernavn",
"password": "Kodeord",
"disable_rtsp": "Deaktiver RTSP-signalet",
"doorbell_text": "Komma separaret liste af tekst der kan vises på Dørklokken. Efterlad tom hvis ingen dørklokke."
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/translations/de.json
================================================
{
"config": {
"abort": {
"server_exists": "UniFi Protect Server bereits konfiguriert."
},
"error": {
"connection_error": "Verbindung fehlgeschlagen. Falsche IP oder Username/Password.",
"protect_version": "UniFi Protect Version is not supported by this Integration",
"nvr_error": "Fehler beim Datenaustausch mit UniFi Protect."
},
"step": {
"user": {
"description": "Set up UniFi Protect to monitor cameras and sensors.",
"title": "UniFi Protect",
"data": {
"host": "IP-Adresse des UniFi Protect Server",
"port": "Portnummer",
"username": "Username",
"password": "Password"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"username": "Username",
"password": "Password",
"disable_rtsp": "Den RTSP-stream deaktivieren",
"doorbell_text": "Add a comma separated list of Custom Texts for the doorbell. Leave blank if no Doorbel"
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/translations/en.json
================================================
{
"config": {
"abort": {
"reauth_successful": "Re-authentication was successful",
"server_exists": "UniFi Protect Server already configured."
},
"error": {
"invalid_auth": "Invalid authentication",
"nvr_error": "Error retrieving data from UniFi Protect.",
"protect_version": "UniFi Protect Version is not supported by this Integration"
},
"step": {
"reauth_confirm": {
"data": {
"host": "IP/Host of UniFi Protect Server",
"password": "Password",
"port": "Port",
"username": "Username"
},
"title": "UniFi Protect Reauth"
},
"user": {
"data": {
"host": "IP/Host of UniFi Protect Server",
"password": "Password",
"port": "Port",
"username": "Username",
"verify_ssl": "Verify SSL certificate"
},
"description": "Set up UniFi Protect to monitor cameras and sensors.",
"title": "UniFi Protect Setup"
}
}
},
"options": {
"step": {
"init": {
"data": {
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
"disable_rtsp": "Disable the RTSP stream",
"override_connection_host": "Override Connection Host"
},
"description": "Realtime metrics option should only be enabled if you have enabled the diagnostics sensors and want them updated in realtime. If if not enabled, they will only update once every 15 minutes.",
"title": "UniFi Protect Options"
}
}
}
}
================================================
FILE: custom_components/unifiprotect/translations/fr.json
================================================
{
"config": {
"abort": {
"server_exists": "Le server UniFi Protect est déjà configuré."
},
"error": {
"connection_error": "Impossible de se connecter. Mauvaise IP ou identifiant/mot de passe.",
"protect_version": "UniFi Protect Version is not supported by this Integration",
"nvr_error": "Erreur lors de la récupération de données sur UniFi Protect."
},
"step": {
"user": {
"description": "Mettre en place UniFi Protect pour surveiller les caméras et les capteurs.",
"title": "UniFi Protect",
"data": {
"host": "Adresse IP de la UniFi Protect Server",
"port": "Numéro de port",
"username": "Identifiant",
"password": "Mot de passe"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"username": "Identifiant",
"password": "Mot de passe",
"disable_rtsp": "Désactiver le flux RTSP",
"doorbell_text": "Add a comma separated list of Custom Texts for the doorbell. Leave blank if no Doorbel"
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/translations/nb.json
================================================
{
"config": {
"abort": {
"server_exists": "UniFi Protect Server er allerede konfigurert."
},
"error": {
"connection_error": "Tilkobling mislyktes. Feil IP eller brukernavn / passord.",
"protect_version": "UniFi Protect Version is not supported by this Integration",
"nvr_error": "Feil ved henting av data fra UniFi Protect."
},
"step": {
"user": {
"description": "Sett opp UniFi Protect for å overvåke kameraer og sensorer.",
"title": "UniFi Protect",
"data": {
"host": "IP-adresse til UniFi Protect Server",
"port": "Portnummer",
"username": "Brukenavn",
"password": "Passord"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"username": "Brukenavn",
"password": "Passord",
"disable_rtsp": "Deaktiver RTSP-signalet",
"doorbell_text": "Add a comma separated list of Custom Texts for the doorbell. Leave blank if no Doorbel"
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/translations/nl.json
================================================
{
"config": {
"abort": {
"server_exists": "UniFi Protect Server is al geconfigureerd."
},
"error": {
"connection_error": "Kon niet verbinden. Verkeerd IP of gebruikersnaam/wachtwoord.",
"protect_version": "UniFi Protect Version is not supported by this Integration",
"nvr_error": "Fout bij het ophalen van gegevens vanuit UniFi Protect."
},
"step": {
"user": {
"description": "Stel UniFi Protect in om cameras and sensoren te gebruiken.",
"title": "UniFi Protect",
"data": {
"host": "IP-Adres van de UniFi Protect Server",
"port": "Poort Nummer",
"username": "Gebruikersnaam",
"password": "Wachtwoord"
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"username": "Gebruikersnaam",
"password": "Wachtwoord",
"disable_rtsp": "Schakel de RTSP-stream uit",
"doorbell_text": "Add a comma separated list of Custom Texts for the doorbell. Leave blank if no Doorbel"
}
}
}
}
}
================================================
FILE: custom_components/unifiprotect/utils.py
================================================
"""UniFi Protect Integration utils."""
from __future__ import annotations
from enum import Enum
from typing import Any
def get_nested_attr(obj: Any, attr: str) -> Any:
"""Fetch a nested attribute."""
attrs = attr.split(".")
value = obj
for key in attrs:
if not hasattr(value, key):
return None
value = getattr(value, key)
if isinstance(value, Enum):
value = value.value
return value
================================================
FILE: hacs.json
================================================
{
"name": "UniFi Protect Integration",
"domains": [
"binary_sensor",
"sensor",
"camera",
"switch",
"light",
"select",
"number"
],
"homeassistant": "2021.11.0",
"iot_class": [
"Local Push"
]
}
================================================
FILE: info.md
================================================
# // UniFi Protect for Home Assistant
 [](https://github.com/custom-components/hacs)
> **NOTE** If you are NOT running UniFi Protect V1.20.0 or higher, you must use the **V0.9.1** of this Integration.
> Please the [CHANGELOG](https://github.com/briis/unifiprotect/blob/master/CHANGELOG.md) very carefully before you upgrade as there are many breaking changes going from V0.9.1 to 0.10.0
>
> You will also need Home Assistant **2021.11+** to upgrade to V0.10.0 as well.
The UniFi Protect Integration adds support for retrieving Camera feeds and Sensor data from a UniFi Protect installation on either a Ubiquiti CloudKey+, Ubiquiti UniFi Dream Machine Pro (UDMP) or UniFi Protect Network Video Recorder (UNVR).
There is support for the following entity types within Home Assistant:
* Camera
* Sensor
* Binary Sensor
* Switch
* Select
* Number
It supports both regular Ubiquiti Cameras and the UniFi Doorbell. Camera feeds, Motion Sensors, Doorbell Sensors, Motion Setting Sensors and Switches will be created automatically for each Camera found, once the Integration has been configured.
Go to [Github](https://github.com/briis/unifiprotect) for Pre-requisites and Setup Instructions.
================================================
FILE: pylintrc
================================================
[MASTER]
reports=no
# Reasons disabled:
# format - handled by black
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# abstract-class-not-used - is flaky, should not show up but does
# unused-argument - generic callbacks and setup methods create a lot of warnings
# global-statement - used for the on-demand requirement installation
# redefined-variable-type - this is Python, we're duck typing!
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# no-self-use - used for common between async and non-async
#
disable=
format,
locally-disabled,
duplicate-code,
cyclic-import,
abstract-class-little-used,
abstract-class-not-used,
unused-argument,
global-statement,
redefined-variable-type,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
too-many-public-methods,
too-many-return-statements,
too-many-statements,
too-many-lines,
too-few-public-methods,
abstract-method,
missing-docstring,
no-self-use
[EXCEPTIONS]
overgeneral-exceptions=Exception
================================================
FILE: pyproject.toml
================================================
[tool.black]
target-version = ["py38"]
exclude = 'generated'
[tool.isort]
# https://github.com/PyCQA/isort/wiki/isort-Settings
profile = "black"
# will group `import x` and `from x import` of the same module.
force_sort_within_sections = true
known_first_party = [
"custom_components",
"tests",
]
forced_separate = [
"tests",
]
combine_as_imports = true
[tool.pylint.MASTER]
ignore = [
"tests",
]
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
# Disabled for now: https://github.com/PyCQA/pylint/issues/3584
#jobs = 2
load-plugins = [
"pylint_strict_informational",
]
persistent = false
extension-pkg-whitelist = [
"ciso8601",
"cv2",
]
[tool.pylint.BASIC]
good-names = [
"_",
"ev",
"ex",
"fp",
"i",
"id",
"j",
"k",
"Run",
"T",
]
[tool.pylint."MESSAGES CONTROL"]
# Reasons disabled:
# format - handled by black
# locally-disabled - it spams too much
# duplicate-code - unavoidable
# cyclic-import - doesn't test if both import on load
# abstract-class-little-used - prevents from setting right foundation
# unused-argument - generic callbacks and setup methods create a lot of warnings
# too-many-* - are not enforced for the sake of readability
# too-few-* - same as too-many-*
# abstract-method - with intro of async there are always methods missing
# inconsistent-return-statements - doesn't handle raise
# too-many-ancestors - it's too strict.
# wrong-import-order - isort guards this
# unexpected-keyword-arg - mypy
disable = [
"format",
"abstract-class-little-used",
"abstract-method",
"cyclic-import",
"duplicate-code",
"inconsistent-return-statements",
"locally-disabled",
"not-context-manager",
"too-few-public-methods",
"too-many-ancestors",
"too-many-arguments",
"too-many-branches",
"too-many-instance-attributes",
"too-many-lines",
"too-many-locals",
"too-many-public-methods",
"too-many-return-statements",
"too-many-statements",
"too-many-boolean-expressions",
"unused-argument",
"wrong-import-order",
"unexpected-keyword-arg",
]
enable = [
#"useless-suppression", # temporarily every now and then to clean them up
"use-symbolic-message-instead",
]
[tool.pylint.REPORTS]
score = false
[tool.pylint.TYPECHECK]
ignored-classes = [
"_CountingAttr", # for attrs
]
[tool.pylint.FORMAT]
expected-line-ending-format = "LF"
[tool.pylint.EXCEPTIONS]
overgeneral-exceptions = [
"BaseException",
"Exception",
"HomeAssistantError",
]
[tool.pytest.ini_options]
testpaths = [
"tests",
]
norecursedirs = [
".git",
"testing_config",
]
================================================
FILE: unifiprotect.markdown
================================================
---
title: Ubiquiti UniFi Protect
description: Instructions on how to configure UniFi Protect integration by Ubiquiti.
ha_category:
- Hub
- Camera
- Light
- Number
- Sensor
- Select
- Switch
ha_release: 2021.11
ha_iot_class: Local Push
ha_config_flow: true
ha_quality_scale: platinum
ha_codeowners:
- '@briis'
ha_domain: unifiprotect
ha_ssdp: true
ha_platforms:
- camera
- binary_sensor
- sensor
- light
- switch
- select
- number
---
The [UniFi Protect Integration](https://ui.com/camera-security) by [Ubiquiti Networks, inc.](https://www.ui.com/), adds support for retrieving Camera feeds and Sensor data from a UniFi Protect installation on either a Ubiquiti CloudKey+, Ubiquiti UniFi Dream Machine Pro or UniFi Protect Network Video Recorder.
There is support for the following device types within Home Assistant:
* Camera
* A camera entity for each camera found on the NVR device will be created
* Sensor
* A sensor for each camera found will be created. This sensor will hold the current recording mode.
* A sensor for each Floodlight device found will be created. This sensor will hold the status of when light will turn on.
* Binary Sensor
* One to two binary sensors will be created per camera found. There will always be a binary sensor recording if motion is detected per camera. If the camera is a doorbell, there will also be a binary sensor created that records if the doorbell is pressed.
* Switch
* For each camera supporting High Dynamic Range (HDR) a switch will be created to turn this setting on or off.
* For each camera supporting High Frame Rate recording a switch will be created to turn this setting on or off.
* For each camera a switch will be created to turn the status light on or off.
* Light
* A light entity will be created for each UniFi Floodlight found. This works as a normal light entity, and has a brightness scale also.
* Select
* For each Camera found there will be a Select entity created from where you can set the cameras recording mode.
* For each Doorbell found, there will be a Select entity created that makes it possible to set the LCD Text. If you make a list of Texts in the Integration configuration, you can both set the standard texts and custom text that you define here.
* For each Camera found there will be a Select entity created from where you can set the behavior of the Infrared light on the Camera
* For each Viewport found, there will be a Select entity from where you change the active View being displayed on the Viewport.
* For each Floodlight device there be a Select entity to set the behavior of the built-in motion sensor.
* Number
* For each camera supporting WDR, a number entity will be setup to set the active value.
* For each camera a number entity will be created from where you can set the microphone sensitivity level.
* For each camera supporting Optical Zoom, a number entity will be setup to set the zoom position.
{% include integrations/config_flow.md %}
### Hardware
This Integration supports all Ubiquiti Hardware that can run UniFi Protect. Currently this includes:
* UniFi Protect Network Video Recorder (**UNVR**)
* UniFi Dream Machine Pro (**UDMP**)
* UniFi Cloud Key Gen2 Plus (**CKGP**) Minimum required Firmware version is **2.0.24** Below that this Integration will not run on a CloudKey+
### Software Versions
* UniFi Protect minimum version is **1.20.0**
* Home Assistant minimum version is **2021.9.0**