Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reolink add binary sensors #85654

Merged
merged 63 commits into from Jan 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
0182164
Add binary_sensor support
starkillerOG Jan 8, 2023
f579193
bump reolink-aio to 0.2.1
starkillerOG Jan 9, 2023
0d72cb1
fix styling
starkillerOG Jan 10, 2023
46b8d81
Update .coveragerc
starkillerOG Jan 10, 2023
85231ee
fix binary_sensors
starkillerOG Jan 10, 2023
3c5fa33
fix handle_event
starkillerOG Jan 10, 2023
893fd10
fix loggers
starkillerOG Jan 10, 2023
4644b86
Merge branch 'dev' into reolink_binary_sensors
starkillerOG Jan 10, 2023
9c5881a
fix import
starkillerOG Jan 10, 2023
89db622
Merge branch 'reolink_binary_sensors' of https://github.com/starkille…
starkillerOG Jan 10, 2023
3eceecf
Update homeassistant/components/reolink/host.py
starkillerOG Jan 11, 2023
fd33504
Update homeassistant/components/reolink/host.py
starkillerOG Jan 11, 2023
8757747
Update homeassistant/components/reolink/binary_sensor.py
starkillerOG Jan 11, 2023
cf6d64d
rename to _async_handle_event
starkillerOG Jan 11, 2023
86890d2
Update homeassistant/components/reolink/binary_sensor.py
starkillerOG Jan 11, 2023
3d5ca7c
rename description to entity_description
starkillerOG Jan 11, 2023
1c08d5e
do not relay on is_on
starkillerOG Jan 11, 2023
ac4d271
unsubscribe listeners
starkillerOG Jan 11, 2023
a983ae2
use async_dispatcher_send instead of hass.bus.async_fire
starkillerOG Jan 11, 2023
bf7357f
No need to update before add
starkillerOG Jan 11, 2023
6949218
No need for update_before_add
starkillerOG Jan 11, 2023
292fa66
Switch to exceptions instead of boolean returns
starkillerOG Jan 13, 2023
562299f
bump reolink-aio to 0.2.2
starkillerOG Jan 13, 2023
1689bc0
Update homeassistant/components/reolink/config_flow.py
starkillerOG Jan 13, 2023
db4e891
Update homeassistant/components/reolink/__init__.py
starkillerOG Jan 13, 2023
ab33173
remove '' in errors
starkillerOG Jan 13, 2023
47ff6df
fix styling and typos
starkillerOG Jan 13, 2023
ea4038c
fix tests
starkillerOG Jan 13, 2023
00b3baf
Merge branch 'dev' into reolink_binary_sensors
starkillerOG Jan 13, 2023
dcc0aa5
fix styling
starkillerOG Jan 13, 2023
4555a78
Merge branch 'dev' into reolink_binary_sensors
starkillerOG Jan 14, 2023
9cc2907
Simplify config flow
starkillerOG Jan 14, 2023
ff77d95
Only log subscription lost once
starkillerOG Jan 14, 2023
a070b37
fix from err instead of from UserNotAdmin
starkillerOG Jan 14, 2023
068739c
Merge branch 'dev' into reolink_binary_sensors
starkillerOG Jan 15, 2023
776bf4e
Update homeassistant/components/reolink/binary_sensor.py
starkillerOG Jan 16, 2023
0a5f375
Update homeassistant/components/reolink/binary_sensor.py
starkillerOG Jan 16, 2023
7e881ca
Update homeassistant/components/reolink/binary_sensor.py
starkillerOG Jan 16, 2023
cec91bf
stricter typing of Callable
starkillerOG Jan 16, 2023
21f2a02
fix mypy
starkillerOG Jan 16, 2023
6d4bf27
fix mypy
starkillerOG Jan 16, 2023
e05cde6
Apply suggestions from code review
starkillerOG Jan 16, 2023
40cdcca
fix typing
starkillerOG Jan 16, 2023
a039b20
Apply suggestions from code review
starkillerOG Jan 17, 2023
be3cdd7
Apply suggestions from code review
starkillerOG Jan 17, 2023
51aacef
fix supported call
starkillerOG Jan 17, 2023
f0c6708
shorten docstring
starkillerOG Jan 17, 2023
4bb2e67
styling suggestion
starkillerOG Jan 17, 2023
1a673c4
Split out NVR and camera entities
starkillerOG Jan 17, 2023
53a5f64
fix
starkillerOG Jan 17, 2023
0c0b69f
fix missing )
starkillerOG Jan 17, 2023
f87645d
fix newline
starkillerOG Jan 17, 2023
71996cf
Merge branch 'dev' into reolink_binary_sensors
starkillerOG Jan 20, 2023
e92fe3d
Apply suggestions from code review
starkillerOG Jan 20, 2023
e9de869
break long lines
starkillerOG Jan 20, 2023
8a14e08
move constants
starkillerOG Jan 20, 2023
95e92f0
clarify hardware camera entity
starkillerOG Jan 20, 2023
420260d
if no body: skip
starkillerOG Jan 20, 2023
5cba3cb
Move renew exceptions to host
starkillerOG Jan 20, 2023
2f9db76
remove not yet used ReolinkNVRCoordinatorEntity
starkillerOG Jan 20, 2023
74a1bba
fix
starkillerOG Jan 20, 2023
20e2bb6
remove device_class for AI sensors
starkillerOG Jan 20, 2023
81f6fee
remove ValueError exception
starkillerOG Jan 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Expand Up @@ -1064,6 +1064,7 @@ omit =
homeassistant/components/remember_the_milk/__init__.py
homeassistant/components/remote_rpi_gpio/*
homeassistant/components/reolink/__init__.py
homeassistant/components/reolink/binary_sensor.py
homeassistant/components/reolink/camera.py
homeassistant/components/reolink/const.py
homeassistant/components/reolink/entity.py
Expand Down
9 changes: 6 additions & 3 deletions homeassistant/components/reolink/__init__.py
Expand Up @@ -30,7 +30,7 @@

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [Platform.CAMERA]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA]
DEVICE_UPDATE_INTERVAL = 60


Expand All @@ -49,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
try:
await host.async_init()
except UserNotAdmin as err:
raise ConfigEntryAuthFailed(err) from UserNotAdmin
raise ConfigEntryAuthFailed(err) from err
except (
ClientConnectorError,
asyncio.TimeoutError,
Expand All @@ -62,7 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
) as err:
await host.stop()
raise ConfigEntryNotReady(
f'Error while trying to setup {host.api.host}:{host.api.port}: "{str(err)}".'
f"Error while trying to setup {host.api.host}:{host.api.port}: {str(err)}"
) from err

config_entry.async_on_unload(
Expand All @@ -79,6 +79,9 @@ async def async_device_config_update():
f"Error updating Reolink {host.api.nvr_name}"
) from err

async with async_timeout.timeout(host.api.timeout):
await host.renew()

coordinator_device_config_update = DataUpdateCoordinator(
hass,
_LOGGER,
Expand Down
168 changes: 168 additions & 0 deletions homeassistant/components/reolink/binary_sensor.py
@@ -0,0 +1,168 @@
"""This component provides support for Reolink binary sensors."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from reolink_aio.api import (
FACE_DETECTION_TYPE,
PERSON_DETECTION_TYPE,
PET_DETECTION_TYPE,
VEHICLE_DETECTION_TYPE,
starkillerOG marked this conversation as resolved.
Show resolved Hide resolved
Host,
)

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import ReolinkData
from .const import DOMAIN
from .entity import ReolinkCoordinatorEntity


@dataclass
class ReolinkBinarySensorEntityDescriptionMixin:
"""Mixin values for Reolink binary sensor entities."""

value: Callable[[Host, int | None], bool]


@dataclass
class ReolinkBinarySensorEntityDescription(
BinarySensorEntityDescription, ReolinkBinarySensorEntityDescriptionMixin
):
"""A class that describes binary sensor entities."""

icon: str = "mdi:motion-sensor"
icon_off: str = "mdi:motion-sensor-off"
supported: Callable[[Host, int | None], bool] = lambda host, ch: True


BINARY_SENSORS = (
ReolinkBinarySensorEntityDescription(
key="motion",
name="Motion",
device_class=BinarySensorDeviceClass.MOTION,
value=lambda api, ch: api.motion_detected(ch),
),
ReolinkBinarySensorEntityDescription(
key=FACE_DETECTION_TYPE,
name="Face",
icon="mdi:face-recognition",
value=lambda api, ch: api.ai_detected(ch, FACE_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, FACE_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=PERSON_DETECTION_TYPE,
name="Person",
value=lambda api, ch: api.ai_detected(ch, PERSON_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, PERSON_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=VEHICLE_DETECTION_TYPE,
name="Vehicle",
icon="mdi:car",
icon_off="mdi:car-off",
value=lambda api, ch: api.ai_detected(ch, VEHICLE_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, VEHICLE_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key=PET_DETECTION_TYPE,
name="Pet",
icon="mdi:dog-side",
icon_off="mdi:dog-side-off",
value=lambda api, ch: api.ai_detected(ch, PET_DETECTION_TYPE),
supported=lambda api, ch: api.ai_supported(ch, PET_DETECTION_TYPE),
),
ReolinkBinarySensorEntityDescription(
key="visitor",
name="Visitor",
icon="mdi:bell-ring-outline",
icon_off="mdi:doorbell",
value=lambda api, ch: api.visitor_detected(ch),
starkillerOG marked this conversation as resolved.
Show resolved Hide resolved
supported=lambda api, ch: api.is_doorbell_enabled(ch),
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Reolink IP Camera."""
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]

entities: list[ReolinkBinarySensorEntity] = []
for channel in reolink_data.host.api.channels:
entities.extend(
[
ReolinkBinarySensorEntity(reolink_data, channel, entity_description)
for entity_description in BINARY_SENSORS
if entity_description.supported(reolink_data.host.api, channel)
]
)

async_add_entities(entities)


class ReolinkBinarySensorEntity(ReolinkCoordinatorEntity, BinarySensorEntity):
"""Base binary-sensor class for Reolink IP camera motion sensors."""

_attr_has_entity_name = True
entity_description: ReolinkBinarySensorEntityDescription

def __init__(
self,
reolink_data: ReolinkData,
channel: int,
entity_description: ReolinkBinarySensorEntityDescription,
) -> None:
"""Initialize Reolink binary sensor."""
super().__init__(reolink_data, channel)
self.entity_description = entity_description

self._attr_unique_id = (
f"{self._host.unique_id}_{self._channel}_{entity_description.key}"
)

@property
def icon(self) -> str | None:
"""Icon of the sensor."""
if self.is_on is False:
return self.entity_description.icon_off
return super().icon

@property
def is_on(self) -> bool:
starkillerOG marked this conversation as resolved.
Show resolved Hide resolved
"""State of the sensor."""
return self.entity_description.value(self._host.api, self._channel)

async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._host.webhook_id}_{self._channel}",
self._async_handle_event,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._host.webhook_id}_all",
self._async_handle_event,
)
)

async def _async_handle_event(self, event):
"""Handle incoming event for motion detection."""
self.async_write_ha_state()
7 changes: 3 additions & 4 deletions homeassistant/components/reolink/camera.py
Expand Up @@ -34,9 +34,9 @@ async def async_setup_entry(
stream_url = await host.api.get_stream_source(channel, stream)
if stream_url is None and stream != "snapshots":
continue
cameras.append(ReolinkCamera(reolink_data, config_entry, channel, stream))
cameras.append(ReolinkCamera(reolink_data, channel, stream))

async_add_entities(cameras, update_before_add=True)
async_add_entities(cameras)


class ReolinkCamera(ReolinkCoordinatorEntity, Camera):
Expand All @@ -48,12 +48,11 @@ class ReolinkCamera(ReolinkCoordinatorEntity, Camera):
def __init__(
self,
reolink_data: ReolinkData,
config_entry: ConfigEntry,
channel: int,
stream: str,
) -> None:
"""Initialize Reolink camera stream."""
ReolinkCoordinatorEntity.__init__(self, reolink_data, config_entry, channel)
ReolinkCoordinatorEntity.__init__(self, reolink_data, channel)
Camera.__init__(self)

self._stream = stream
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/reolink/config_flow.py
Expand Up @@ -14,12 +14,13 @@
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv

from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DEFAULT_PROTOCOL, DOMAIN
from .const import CONF_PROTOCOL, CONF_USE_HTTPS, DOMAIN
from .exceptions import ReolinkException, UserNotAdmin
from .host import ReolinkHost

_LOGGER = logging.getLogger(__name__)

DEFAULT_PROTOCOL = "rtsp"
DEFAULT_OPTIONS = {CONF_PROTOCOL: DEFAULT_PROTOCOL}


Expand Down
3 changes: 0 additions & 3 deletions homeassistant/components/reolink/const.py
Expand Up @@ -4,6 +4,3 @@

CONF_USE_HTTPS = "use_https"
CONF_PROTOCOL = "protocol"

DEFAULT_PROTOCOL = "rtsp"
DEFAULT_TIMEOUT = 60
11 changes: 4 additions & 7 deletions homeassistant/components/reolink/entity.py
@@ -1,7 +1,6 @@
"""Reolink parent entity class."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
Expand All @@ -11,12 +10,10 @@


class ReolinkCoordinatorEntity(CoordinatorEntity):
"""Parent class for Reolink Entities."""
"""Parent class for Reolink hardware camera entities."""

def __init__(
self, reolink_data: ReolinkData, config_entry: ConfigEntry, channel: int | None
) -> None:
"""Initialize ReolinkCoordinatorEntity."""
def __init__(self, reolink_data: ReolinkData, channel: int) -> None:
"""Initialize ReolinkCoordinatorEntity for a hardware camera."""
coordinator = reolink_data.device_coordinator
super().__init__(coordinator)

Expand All @@ -25,7 +22,7 @@ def __init__(

http_s = "https" if self._host.api.use_https else "http"
conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
if self._host.api.is_nvr and self._channel is not None:
if self._host.api.is_nvr:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{self._host.unique_id}_ch{self._channel}")},
via_device=(DOMAIN, self._host.unique_id),
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/reolink/exceptions.py
Expand Up @@ -10,5 +10,9 @@ class ReolinkSetupException(ReolinkException):
"""Raised when setting up the Reolink host failed."""


class ReolinkWebhookException(ReolinkException):
"""Raised when registering the reolink webhook failed."""


class UserNotAdmin(ReolinkException):
"""Raised when user is not admin."""