diff --git a/.coveragerc b/.coveragerc index e9c34e4d8fd..6ef51578a5c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -982,6 +982,7 @@ omit = homeassistant/components/reolink/entity.py homeassistant/components/reolink/host.py homeassistant/components/reolink/number.py + homeassistant/components/reolink/switch.py homeassistant/components/reolink/update.py homeassistant/components/repetier/__init__.py homeassistant/components/repetier/sensor.py diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index 7de11239560..f6a5e197644 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -28,6 +28,7 @@ PLATFORMS = [ Platform.BUTTON, Platform.CAMERA, Platform.NUMBER, + Platform.SWITCH, Platform.UPDATE, ] DEVICE_UPDATE_INTERVAL = timedelta(seconds=60) diff --git a/homeassistant/components/reolink/switch.py b/homeassistant/components/reolink/switch.py new file mode 100644 index 00000000000..64d61554856 --- /dev/null +++ b/homeassistant/components/reolink/switch.py @@ -0,0 +1,239 @@ +"""Component providing support for Reolink switch entities.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from reolink_aio.api import Host + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import ReolinkData +from .const import DOMAIN +from .entity import ReolinkBaseCoordinatorEntity, ReolinkCoordinatorEntity + + +@dataclass +class ReolinkSwitchEntityDescriptionMixin: + """Mixin values for Reolink switch entities.""" + + value: Callable[[Host, int], bool] + method: Callable[[Host, int, bool], Any] + + +@dataclass +class ReolinkSwitchEntityDescription( + SwitchEntityDescription, ReolinkSwitchEntityDescriptionMixin +): + """A class that describes switch entities.""" + + supported: Callable[[Host, int], bool] = lambda api, ch: True + + +@dataclass +class ReolinkNVRSwitchEntityDescriptionMixin: + """Mixin values for Reolink NVR switch entities.""" + + value: Callable[[Host], bool] + method: Callable[[Host, bool], Any] + + +@dataclass +class ReolinkNVRSwitchEntityDescription( + SwitchEntityDescription, ReolinkNVRSwitchEntityDescriptionMixin +): + """A class that describes NVR switch entities.""" + + supported: Callable[[Host], bool] = lambda api: True + + +SWITCH_ENTITIES = ( + ReolinkSwitchEntityDescription( + key="record_audio", + name="Record audio", + icon="mdi:microphone", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "audio"), + value=lambda api, ch: api.audio_record(ch), + method=lambda api, ch, value: api.set_audio(ch, value), + ), + ReolinkSwitchEntityDescription( + key="siren_on_event", + name="Siren on event", + icon="mdi:alarm-light", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "siren"), + value=lambda api, ch: api.audio_alarm_enabled(ch), + method=lambda api, ch, value: api.set_audio_alarm(ch, value), + ), + ReolinkSwitchEntityDescription( + key="auto_tracking", + name="Auto tracking", + icon="mdi:target-account", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "auto_track"), + value=lambda api, ch: api.auto_track_enabled(ch), + method=lambda api, ch, value: api.set_auto_tracking(ch, value), + ), + ReolinkSwitchEntityDescription( + key="auto_focus", + name="Auto focus", + icon="mdi:focus-field", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "auto_focus"), + value=lambda api, ch: api.autofocus_enabled(ch), + method=lambda api, ch, value: api.set_autofocus(ch, value), + ), + ReolinkSwitchEntityDescription( + key="gaurd_return", + name="Guard return", + icon="mdi:crosshairs-gps", + entity_category=EntityCategory.CONFIG, + supported=lambda api, ch: api.supported(ch, "ptz_guard"), + value=lambda api, ch: api.ptz_guard_enabled(ch), + method=lambda api, ch, value: api.set_ptz_guard(ch, enable=value), + ), +) + +NVR_SWITCH_ENTITIES = ( + ReolinkNVRSwitchEntityDescription( + key="email", + name="Email on event", + icon="mdi:email", + entity_category=EntityCategory.CONFIG, + supported=lambda api: api.supported(None, "email"), + value=lambda api: api.email_enabled(), + method=lambda api, value: api.set_email(None, value), + ), + ReolinkNVRSwitchEntityDescription( + key="ftp_upload", + name="FTP upload", + icon="mdi:swap-horizontal", + entity_category=EntityCategory.CONFIG, + supported=lambda api: api.supported(None, "ftp"), + value=lambda api: api.ftp_enabled(), + method=lambda api, value: api.set_ftp(None, value), + ), + ReolinkNVRSwitchEntityDescription( + key="push_notifications", + name="Push notifications", + icon="mdi:message-badge", + entity_category=EntityCategory.CONFIG, + supported=lambda api: api.supported(None, "push"), + value=lambda api: api.push_enabled(), + method=lambda api, value: api.set_push(None, value), + ), + ReolinkNVRSwitchEntityDescription( + key="record", + name="Record", + icon="mdi:record-rec", + supported=lambda api: api.supported(None, "recording"), + value=lambda api: api.recording_enabled(), + method=lambda api, value: api.set_recording(None, value), + ), + ReolinkNVRSwitchEntityDescription( + key="buzzer", + name="Buzzer on event", + icon="mdi:room-service", + entity_category=EntityCategory.CONFIG, + supported=lambda api: api.supported(None, "buzzer"), + value=lambda api: api.buzzer_enabled(), + method=lambda api, value: api.set_buzzer(None, value), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up a Reolink switch entities.""" + reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[ReolinkSwitchEntity | ReolinkNVRSwitchEntity] = [ + ReolinkSwitchEntity(reolink_data, channel, entity_description) + for entity_description in SWITCH_ENTITIES + for channel in reolink_data.host.api.channels + if entity_description.supported(reolink_data.host.api, channel) + ] + entities.extend( + [ + ReolinkNVRSwitchEntity(reolink_data, entity_description) + for entity_description in NVR_SWITCH_ENTITIES + if entity_description.supported(reolink_data.host.api) + ] + ) + async_add_entities(entities) + + +class ReolinkSwitchEntity(ReolinkCoordinatorEntity, SwitchEntity): + """Base switch entity class for Reolink IP cameras.""" + + entity_description: ReolinkSwitchEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + channel: int, + entity_description: ReolinkSwitchEntityDescription, + ) -> None: + """Initialize Reolink switch entity.""" + super().__init__(reolink_data, channel) + self.entity_description = entity_description + + self._attr_unique_id = ( + f"{self._host.unique_id}_{channel}_{entity_description.key}" + ) + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self.entity_description.value(self._host.api, self._channel) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.method(self._host.api, self._channel, True) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.method(self._host.api, self._channel, False) + self.async_write_ha_state() + + +class ReolinkNVRSwitchEntity(ReolinkBaseCoordinatorEntity, SwitchEntity): + """Switch entity class for Reolink NVR features.""" + + entity_description: ReolinkNVRSwitchEntityDescription + + def __init__( + self, + reolink_data: ReolinkData, + entity_description: ReolinkNVRSwitchEntityDescription, + ) -> None: + """Initialize Reolink switch entity.""" + super().__init__(reolink_data) + self.entity_description = entity_description + + self._attr_unique_id = f"{self._host.unique_id}_{entity_description.key}" + + @property + def is_on(self) -> bool: + """Return true if switch is on.""" + return self.entity_description.value(self._host.api) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + await self.entity_description.method(self._host.api, True) + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_description.method(self._host.api, False) + self.async_write_ha_state()