diff --git a/.coveragerc b/.coveragerc index d36153ccca7..e64058d93d0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -341,6 +341,7 @@ omit = homeassistant/components/ezviz/entity.py homeassistant/components/ezviz/select.py homeassistant/components/ezviz/sensor.py + homeassistant/components/ezviz/siren.py homeassistant/components/ezviz/switch.py homeassistant/components/ezviz/update.py homeassistant/components/faa_delays/__init__.py diff --git a/homeassistant/components/ezviz/__init__.py b/homeassistant/components/ezviz/__init__.py index c007de78130..12754af25e8 100644 --- a/homeassistant/components/ezviz/__init__.py +++ b/homeassistant/components/ezviz/__init__.py @@ -42,6 +42,7 @@ PLATFORMS_BY_TYPE: dict[str, list] = { Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SIREN, Platform.SWITCH, Platform.UPDATE, ], diff --git a/homeassistant/components/ezviz/camera.py b/homeassistant/components/ezviz/camera.py index 083e433952f..85b1f316a7b 100644 --- a/homeassistant/components/ezviz/camera.py +++ b/homeassistant/components/ezviz/camera.py @@ -288,6 +288,17 @@ class EzvizCamera(EzvizEntity, Camera): def perform_sound_alarm(self, enable: int) -> None: """Sound the alarm on a camera.""" + ir.async_create_issue( + self.hass, + DOMAIN, + "service_depreciation_sound_alarm", + breaks_in_ha_version="2024.3.0", + is_fixable=True, + is_persistent=True, + severity=ir.IssueSeverity.WARNING, + translation_key="service_depreciation_sound_alarm", + ) + try: self.coordinator.ezviz_client.sound_alarm(self._serial, enable) except HTTPError as err: diff --git a/homeassistant/components/ezviz/entity.py b/homeassistant/components/ezviz/entity.py index 6fad2b57142..c8ce3daf074 100644 --- a/homeassistant/components/ezviz/entity.py +++ b/homeassistant/components/ezviz/entity.py @@ -45,6 +45,8 @@ class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity): class EzvizBaseEntity(Entity): """Generic entity for EZVIZ individual poll entities.""" + _attr_has_entity_name = True + def __init__( self, coordinator: EzvizDataUpdateCoordinator, diff --git a/homeassistant/components/ezviz/siren.py b/homeassistant/components/ezviz/siren.py new file mode 100644 index 00000000000..1f08b389236 --- /dev/null +++ b/homeassistant/components/ezviz/siren.py @@ -0,0 +1,133 @@ +"""Support for EZVIZ sirens.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +from typing import Any + +from pyezviz import HTTPError, PyEzvizError, SupportExt + +from homeassistant.components.siren import ( + SirenEntity, + SirenEntityDescription, + SirenEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback +import homeassistant.helpers.event as evt +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import DATA_COORDINATOR, DOMAIN +from .coordinator import EzvizDataUpdateCoordinator +from .entity import EzvizBaseEntity + +PARALLEL_UPDATES = 1 +OFF_DELAY = timedelta(seconds=60) # Camera firmware has hard coded turn off. + +SIREN_ENTITY_TYPE = SirenEntityDescription( + key="siren", + translation_key="siren", +) + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up EZVIZ sensors based on a config entry.""" + coordinator: EzvizDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ + DATA_COORDINATOR + ] + + async_add_entities( + EzvizSirenEntity(coordinator, camera, SIREN_ENTITY_TYPE) + for camera in coordinator.data + for capability, value in coordinator.data[camera]["supportExt"].items() + if capability == str(SupportExt.SupportActiveDefense.value) + if value != "0" + ) + + +class EzvizSirenEntity(EzvizBaseEntity, SirenEntity, RestoreEntity): + """Representation of a EZVIZ Siren entity.""" + + _attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + _attr_should_poll = False + _attr_assumed_state = True + + def __init__( + self, + coordinator: EzvizDataUpdateCoordinator, + serial: str, + description: SirenEntityDescription, + ) -> None: + """Initialize the Siren.""" + super().__init__(coordinator, serial) + self._attr_unique_id = f"{serial}_{description.key}" + self.entity_description = description + self._attr_is_on = False + self._delay_listener: Callable | None = None + + async def async_added_to_hass(self) -> None: + """Run when entity about to be added to hass.""" + if not (last_state := await self.async_get_last_state()): + return + self._attr_is_on = last_state.state == STATE_ON + + if self._attr_is_on: + evt.async_call_later(self.hass, OFF_DELAY, self.off_delay_listener) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off camera siren.""" + try: + result = await self.hass.async_add_executor_job( + self.coordinator.ezviz_client.sound_alarm, self._serial, 1 + ) + + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Failed to turn siren off for {self.name}" + ) from err + + if result: + if self._delay_listener is not None: + self._delay_listener() + self._delay_listener = None + + self._attr_is_on = False + self.async_write_ha_state() + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on camera siren.""" + try: + result = self.hass.async_add_executor_job( + self.coordinator.ezviz_client.sound_alarm, self._serial, 2 + ) + + except (HTTPError, PyEzvizError) as err: + raise HomeAssistantError( + f"Failed to turn siren on for {self.name}" + ) from err + + if result: + if self._delay_listener is not None: + self._delay_listener() + self._delay_listener = None + + self._attr_is_on = True + self._delay_listener = evt.async_call_later( + self.hass, OFF_DELAY, self.off_delay_listener + ) + self.async_write_ha_state() + + @callback + def off_delay_listener(self, now: datetime) -> None: + """Switch device off after a delay. + + Camera firmware has hard coded turn off after 60 seconds. + """ + self._attr_is_on = False + self._delay_listener = None + self.async_write_ha_state() diff --git a/homeassistant/components/ezviz/strings.json b/homeassistant/components/ezviz/strings.json index 3e8797e7c02..373f9af22fc 100644 --- a/homeassistant/components/ezviz/strings.json +++ b/homeassistant/components/ezviz/strings.json @@ -92,6 +92,17 @@ } } } + }, + "service_depreciation_sound_alarm": { + "title": "Ezviz Sound alarm service is being removed", + "fix_flow": { + "step": { + "confirm": { + "title": "[%key:component::ezviz::issues::service_depreciation_sound_alarm::title%]", + "description": "Ezviz Sound alarm service is deprecated and will be removed.\nTo sound the alarm, you can instead use the `siren.toggle` service targeting the Siren entity.\n\nPlease remove the use of this service from your automations and scripts and select **submit** to fix this issue." + } + } + } } }, "entity": { @@ -216,6 +227,11 @@ "firmware": { "name": "[%key:component::update::entity_component::firmware::name%]" } + }, + "siren": { + "siren": { + "name": "[%key:component::siren::title%]" + } } }, "services": {