diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index 8d9f1e55581..6e9c9c2e386 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -7,22 +7,31 @@ from typing import Any from reolink_aio.api import GuardEnum, Host, PtzEnum from reolink_aio.exceptions import ReolinkError +import voluptuous as vol from homeassistant.components.button import ( ButtonDeviceClass, ButtonEntity, ButtonEntityDescription, ) +from homeassistant.components.camera import CameraEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from . import ReolinkData from .const import DOMAIN from .entity import ReolinkChannelCoordinatorEntity, ReolinkHostCoordinatorEntity +ATTR_SPEED = "speed" +SUPPORT_PTZ_SPEED = CameraEntityFeature.STREAM + @dataclass(kw_only=True) class ReolinkButtonEntityDescription( @@ -33,6 +42,7 @@ class ReolinkButtonEntityDescription( enabled_default: Callable[[Host, int], bool] | None = None method: Callable[[Host, int], Any] supported: Callable[[Host, int], bool] = lambda api, ch: True + ptz_cmd: str | None = None @dataclass(kw_only=True) @@ -60,6 +70,7 @@ BUTTON_ENTITIES = ( icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value), + ptz_cmd=PtzEnum.left.value, ), ReolinkButtonEntityDescription( key="ptz_right", @@ -67,6 +78,7 @@ BUTTON_ENTITIES = ( icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "pan"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value), + ptz_cmd=PtzEnum.right.value, ), ReolinkButtonEntityDescription( key="ptz_up", @@ -74,6 +86,7 @@ BUTTON_ENTITIES = ( icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value), + ptz_cmd=PtzEnum.up.value, ), ReolinkButtonEntityDescription( key="ptz_down", @@ -81,6 +94,7 @@ BUTTON_ENTITIES = ( icon="mdi:pan", supported=lambda api, ch: api.supported(ch, "tilt"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), + ptz_cmd=PtzEnum.down.value, ), ReolinkButtonEntityDescription( key="ptz_zoom_in", @@ -89,6 +103,7 @@ BUTTON_ENTITIES = ( entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomin.value), + ptz_cmd=PtzEnum.zoomin.value, ), ReolinkButtonEntityDescription( key="ptz_zoom_out", @@ -97,6 +112,7 @@ BUTTON_ENTITIES = ( entity_registry_enabled_default=False, supported=lambda api, ch: api.supported(ch, "zoom_basic"), method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.zoomout.value), + ptz_cmd=PtzEnum.zoomout.value, ), ReolinkButtonEntityDescription( key="ptz_calibrate", @@ -158,6 +174,14 @@ async def async_setup_entry( ) async_add_entities(entities) + platform = async_get_current_platform() + platform.async_register_entity_service( + "ptz_move", + {vol.Required(ATTR_SPEED): cv.positive_int}, + "async_ptz_move", + [SUPPORT_PTZ_SPEED], + ) + class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): """Base button entity class for Reolink IP cameras.""" @@ -182,6 +206,12 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): entity_description.enabled_default(self._host.api, self._channel) ) + if ( + self._host.api.supported(channel, "ptz_speed") + and entity_description.ptz_cmd is not None + ): + self._attr_supported_features = SUPPORT_PTZ_SPEED + async def async_press(self) -> None: """Execute the button action.""" try: @@ -189,6 +219,16 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): except ReolinkError as err: raise HomeAssistantError(err) from err + async def async_ptz_move(self, **kwargs) -> None: + """PTZ move with speed.""" + speed = kwargs[ATTR_SPEED] + try: + await self._host.api.set_ptz_command( + self._channel, command=self.entity_description.ptz_cmd, speed=speed + ) + except ReolinkError as err: + raise HomeAssistantError(err) from err + class ReolinkHostButtonEntity(ReolinkHostCoordinatorEntity, ButtonEntity): """Base button entity class for Reolink IP cameras.""" diff --git a/homeassistant/components/reolink/services.yaml b/homeassistant/components/reolink/services.yaml new file mode 100644 index 00000000000..42b9af34eb0 --- /dev/null +++ b/homeassistant/components/reolink/services.yaml @@ -0,0 +1,18 @@ +# Describes the format for available reolink services + +ptz_move: + target: + entity: + integration: reolink + domain: button + supported_features: + - camera.CameraEntityFeature.STREAM + fields: + speed: + required: true + default: 10 + selector: + number: + min: 1 + max: 64 + step: 1 diff --git a/homeassistant/components/reolink/strings.json b/homeassistant/components/reolink/strings.json index d81e25e9887..5b26d70b657 100644 --- a/homeassistant/components/reolink/strings.json +++ b/homeassistant/components/reolink/strings.json @@ -61,6 +61,18 @@ "description": "\"{name}\" with model \"{model}\" and hardware version \"{hw_version}\" is running a old firmware version \"{current_firmware}\", while at least firmware version \"{required_firmware}\" is required for proper operation of the Reolink integration. The latest firmware can be downloaded from the [Reolink download center]({download_link})." } }, + "services": { + "ptz_move": { + "name": "PTZ move", + "description": "Move the camera with a specific speed.", + "fields": { + "speed": { + "name": "Speed", + "description": "PTZ move speed." + } + } + } + }, "entity": { "binary_sensor": { "face": {