diff --git a/homeassistant/components/reolink/button.py b/homeassistant/components/reolink/button.py index fb2d4dd2ba74..45b0616770c5 100644 --- a/homeassistant/components/reolink/button.py +++ b/homeassistant/components/reolink/button.py @@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from .const import SUPPORT_PTZ_SPEED from .entity import ( ReolinkChannelCoordinatorEntity, - ReolinkChannelEntityDescription, + ReolinkEntityDescription, ReolinkHostCoordinatorEntity, ReolinkHostEntityDescription, ) @@ -30,12 +30,13 @@ PARALLEL_UPDATES = 0 @dataclass(frozen=True, kw_only=True) class ReolinkButtonEntityDescription( ButtonEntityDescription, - ReolinkChannelEntityDescription, + ReolinkEntityDescription, ): """A class that describes button entities for a camera channel.""" - enabled_default: Callable[[Host, int], bool] | None = None - method: Callable[[Host, int], Any] + supported: Callable[[Host, int, int | None], bool] = lambda api, ch, sub: True + enabled_default: Callable[[Host, int, int | None], bool] | None = None + method: Callable[[Host, int, int | None], Any] ptz_cmd: str | None = None @@ -53,113 +54,141 @@ BUTTON_ENTITIES = ( ReolinkButtonEntityDescription( key="ptz_stop", translation_key="ptz_stop", - enabled_default=lambda api, ch: api.supported(ch, "pan_tilt"), - supported=lambda api, ch: api.supported(ch, "ptz_stop"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.stop.value), + enabled_default=lambda api, ch, sub: api.supported(ch, "pan_tilt", sub), + supported=lambda api, ch, sub: api.supported(ch, "ptz_stop", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.stop.value + ), ), ReolinkButtonEntityDescription( key="ptz_left", translation_key="ptz_left", - supported=lambda api, ch: api.supported(ch, "pan"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.left.value), + supported=lambda api, ch, sub: api.supported(ch, "pan", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.left.value + ), ptz_cmd=PtzEnum.left.value, ), ReolinkButtonEntityDescription( key="ptz_right", translation_key="ptz_right", - supported=lambda api, ch: api.supported(ch, "pan"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.right.value), + supported=lambda api, ch, sub: api.supported(ch, "pan", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.right.value + ), ptz_cmd=PtzEnum.right.value, ), ReolinkButtonEntityDescription( key="ptz_up", translation_key="ptz_up", - supported=lambda api, ch: api.supported(ch, "tilt"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.up.value), + supported=lambda api, ch, sub: api.supported(ch, "tilt", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.up.value + ), ptz_cmd=PtzEnum.up.value, ), ReolinkButtonEntityDescription( key="ptz_down", translation_key="ptz_down", - supported=lambda api, ch: api.supported(ch, "tilt"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.down.value), + supported=lambda api, ch, sub: api.supported(ch, "tilt", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.down.value + ), ptz_cmd=PtzEnum.down.value, ), ReolinkButtonEntityDescription( key="ptz_zoom_in", translation_key="ptz_zoom_in", 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), + supported=lambda api, ch, sub: api.supported(ch, "zoom_basic", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.zoomin.value + ), ptz_cmd=PtzEnum.zoomin.value, ), ReolinkButtonEntityDescription( key="ptz_zoom_out", translation_key="ptz_zoom_out", 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), + supported=lambda api, ch, sub: api.supported(ch, "zoom_basic", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.zoomout.value + ), ptz_cmd=PtzEnum.zoomout.value, ), ReolinkButtonEntityDescription( key="ptz_left_up", translation_key="ptz_left_up", entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "ptz_diagonal"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.leftup.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_diagonal", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.leftup.value + ), ptz_cmd=PtzEnum.leftup.value, ), ReolinkButtonEntityDescription( key="ptz_left_down", translation_key="ptz_left_down", entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "ptz_diagonal"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.leftdown.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_diagonal", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.leftdown.value + ), ptz_cmd=PtzEnum.leftdown.value, ), ReolinkButtonEntityDescription( key="ptz_right_up", translation_key="ptz_right_up", entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "ptz_diagonal"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.rightup.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_diagonal", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.rightup.value + ), ptz_cmd=PtzEnum.rightup.value, ), ReolinkButtonEntityDescription( key="ptz_right_down", translation_key="ptz_right_down", entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "ptz_diagonal"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.rightdown.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_diagonal", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.rightdown.value + ), ptz_cmd=PtzEnum.rightdown.value, ), ReolinkButtonEntityDescription( key="ptz_auto", translation_key="ptz_auto", entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "ptz_auto"), - method=lambda api, ch: api.set_ptz_command(ch, command=PtzEnum.auto.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_auto", sub), + method=lambda api, ch, sub: api.set_ptz_command( + ch, sub, command=PtzEnum.auto.value + ), ptz_cmd=PtzEnum.auto.value, ), ReolinkButtonEntityDescription( key="ptz_calibrate", translation_key="ptz_calibrate", entity_category=EntityCategory.CONFIG, - supported=lambda api, ch: api.supported(ch, "ptz_callibrate"), - method=lambda api, ch: api.ptz_callibrate(ch), + supported=lambda api, ch, sub: api.supported(ch, "ptz_callibrate", sub), + method=lambda api, ch, sub: api.ptz_callibrate(ch, sub), ), ReolinkButtonEntityDescription( key="guard_go_to", translation_key="guard_go_to", - supported=lambda api, ch: api.supported(ch, "ptz_guard"), - method=lambda api, ch: api.set_ptz_guard(ch, command=GuardEnum.goto.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_guard", sub), + method=lambda api, ch, sub: api.set_ptz_guard( + ch, sub, command=GuardEnum.goto.value + ), ), ReolinkButtonEntityDescription( key="guard_set", translation_key="guard_set", entity_category=EntityCategory.CONFIG, - supported=lambda api, ch: api.supported(ch, "ptz_guard"), - method=lambda api, ch: api.set_ptz_guard(ch, command=GuardEnum.set.value), + supported=lambda api, ch, sub: api.supported(ch, "ptz_guard", sub), + method=lambda api, ch, sub: api.set_ptz_guard( + ch, sub, command=GuardEnum.set.value + ), ), ReolinkButtonEntityDescription( key="reboot", @@ -167,14 +196,14 @@ BUTTON_ENTITIES = ( device_class=ButtonDeviceClass.RESTART, entity_category=EntityCategory.CONFIG, entity_registry_enabled_default=False, - supported=lambda api, ch: api.supported(ch, "reboot"), - method=lambda api, ch: api.reboot(ch), + supported=lambda api, ch, sub: api.supported(ch, "reboot", sub), + method=lambda api, ch, sub: api.reboot(ch), ), ReolinkButtonEntityDescription( key="pre_siren", translation_key="pre_siren", - supported=lambda api, ch: api.supported(ch, "pre_siren"), - method=lambda api, ch: api.baichuan.PreAlarm(ch), + supported=lambda api, ch, sub: api.supported(ch, "pre_siren", sub), + method=lambda api, ch, sub: api.baichuan.PreAlarm(ch), ), ) @@ -208,10 +237,11 @@ async def async_setup_entry( reolink_data: ReolinkData = config_entry.runtime_data entities: list[ReolinkButtonEntity | ReolinkHostButtonEntity] = [ - ReolinkButtonEntity(reolink_data, channel, entity_description) + ReolinkButtonEntity(reolink_data, channel, sub_channel, entity_description) for entity_description in BUTTON_ENTITIES for channel in reolink_data.host.api.stream_channels - if entity_description.supported(reolink_data.host.api, channel) + for sub_channel in reolink_data.host.api.sub_channels(channel) + if entity_description.supported(reolink_data.host.api, channel, sub_channel) ] entities.extend( ReolinkHostButtonEntity(reolink_data, entity_description) @@ -230,15 +260,18 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): self, reolink_data: ReolinkData, channel: int, + sub_channel: int | None, entity_description: ReolinkButtonEntityDescription, ) -> None: """Initialize Reolink button entity.""" self.entity_description = entity_description - super().__init__(reolink_data, channel) + super().__init__(reolink_data, channel, sub_channel) if entity_description.enabled_default is not None: self._attr_entity_registry_enabled_default = ( - entity_description.enabled_default(self._host.api, self._channel) + entity_description.enabled_default( + self._host.api, self._channel, self._sub_channel + ) ) if ( @@ -251,13 +284,18 @@ class ReolinkButtonEntity(ReolinkChannelCoordinatorEntity, ButtonEntity): @override async def async_press(self) -> None: """Execute the button action.""" - await self.entity_description.method(self._host.api, self._channel) + await self.entity_description.method( + self._host.api, self._channel, self._sub_channel + ) @raise_translated_error async def async_ptz_move(self, speed: int) -> None: """PTZ move with speed.""" await self._host.api.set_ptz_command( - self._channel, command=self.entity_description.ptz_cmd, speed=speed + self._channel, + self._sub_channel, + command=self.entity_description.ptz_cmd, + speed=speed, ) diff --git a/homeassistant/components/reolink/entity.py b/homeassistant/components/reolink/entity.py index 16e85a943c4b..7fb4c430eca6 100644 --- a/homeassistant/components/reolink/entity.py +++ b/homeassistant/components/reolink/entity.py @@ -174,22 +174,24 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity): self, reolink_data: ReolinkData, channel: int, + sub_channel: int | None = None, coordinator: ReolinkCoordinator | None = None, ) -> None: """Initialize ReolinkChannelCoordinatorEntity.""" super().__init__(reolink_data, coordinator) self._channel = channel + self._sub_channel = sub_channel + sub_id = f"_sub{sub_channel:02d}" if sub_channel is not None else "" if self._host.api.is_nvr and self._host.api.supported(channel, "UID"): self._attr_unique_id = ( f"{self._host.unique_id}" f"_{self._host.api.camera_uid(channel)}" + f"{sub_id}" f"_{self.entity_description.key}" ) else: - self._attr_unique_id = ( - f"{self._host.unique_id}_{channel}_{self.entity_description.key}" - ) + self._attr_unique_id = f"{self._host.unique_id}_{channel}{sub_id}_{self.entity_description.key}" if self._host.api.is_nvr: if self._host.api.supported(channel, "UID"): @@ -338,7 +340,7 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity): ) -> None: """Initialize ReolinkChimeCoordinatorEntity for a chime.""" assert chime.channel is not None - super().__init__(reolink_data, chime.channel, coordinator) + super().__init__(reolink_data, chime.channel, coordinator=coordinator) self._chime = chime self._attr_unique_id = ( diff --git a/homeassistant/components/reolink/update.py b/homeassistant/components/reolink/update.py index 5035ab733c54..7b379ddaf0f7 100644 --- a/homeassistant/components/reolink/update.py +++ b/homeassistant/components/reolink/update.py @@ -292,7 +292,7 @@ class ReolinkUpdateEntity( self, reolink_data, channel, reolink_data.firmware_coordinator ) ReolinkChannelCoordinatorEntity.__init__( - self, reolink_data, channel, reolink_data.firmware_coordinator + self, reolink_data, channel, coordinator=reolink_data.firmware_coordinator ) diff --git a/tests/components/reolink/conftest.py b/tests/components/reolink/conftest.py index 16f5c76d3c2a..8e43a077a7e2 100644 --- a/tests/components/reolink/conftest.py +++ b/tests/components/reolink/conftest.py @@ -112,6 +112,7 @@ def _init_host_mock(host_mock: MagicMock) -> None: host_mock.protocol = "rtsp" host_mock.channels = [0] host_mock.stream_channels = [0] + host_mock.sub_channels.return_value = {None} host_mock.num_cameras = 1 host_mock.new_devices = False host_mock.sw_version_update_required = False diff --git a/tests/components/reolink/snapshots/test_button.ambr b/tests/components/reolink/snapshots/test_button.ambr index be44cf72fd9f..e95e68c682dc 100644 --- a/tests/components/reolink/snapshots/test_button.ambr +++ b/tests/components/reolink/snapshots/test_button.ambr @@ -283,7 +283,7 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ptz_down', - 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_ptz_down', + 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_sub01_ptz_down', 'unit_of_measurement': None, }) # --- @@ -334,7 +334,7 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ptz_left', - 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_ptz_left', + 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_sub01_ptz_left', 'unit_of_measurement': None, }) # --- @@ -487,7 +487,7 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ptz_right', - 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_ptz_right', + 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_sub01_ptz_right', 'unit_of_measurement': None, }) # --- @@ -640,7 +640,7 @@ 'suggested_object_id': None, 'supported_features': 0, 'translation_key': 'ptz_stop', - 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_ptz_stop', + 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_sub01_ptz_stop', 'unit_of_measurement': None, }) # --- @@ -690,7 +690,7 @@ 'suggested_object_id': None, 'supported_features': , 'translation_key': 'ptz_up', - 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_ptz_up', + 'unique_id': 'ABC1234567D89EFG_DEF7654321D89GHT_sub01_ptz_up', 'unit_of_measurement': None, }) # --- diff --git a/tests/components/reolink/test_button.py b/tests/components/reolink/test_button.py index fa3d832c8fb1..07b7a4dab860 100644 --- a/tests/components/reolink/test_button.py +++ b/tests/components/reolink/test_button.py @@ -27,8 +27,20 @@ async def test_all_entities( snapshot: SnapshotAssertion, config_entry: MockConfigEntry, entity_registry: er.EntityRegistry, + reolink_host: MagicMock, ) -> None: """Test all entities.""" + + def mock_supported(ch, capability, sub_channel=None): + if capability in {"ptz_stop", "pan", "tilt"}: + return sub_channel == 1 + if sub_channel is not None: + return False + return True + + reolink_host.sub_channels.return_value = {None, 1} + reolink_host.supported = mock_supported + with patch( "homeassistant.components.reolink.PLATFORMS", [Platform.BUTTON], @@ -68,12 +80,22 @@ async def test_button( ) +@pytest.mark.parametrize( + "sub_channel", + [ + None, + 1, + ], +) async def test_ptz_move_service( hass: HomeAssistant, config_entry: MockConfigEntry, reolink_host: MagicMock, + sub_channel: int | None, ) -> None: """Test ptz_move entity service using PTZ button entity.""" + reolink_host.sub_channels.return_value = {sub_channel} + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BUTTON]): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -87,7 +109,9 @@ async def test_ptz_move_service( {ATTR_ENTITY_ID: entity_id, ATTR_SPEED: 5}, blocking=True, ) - reolink_host.set_ptz_command.assert_called_with(0, command="Up", speed=5) + reolink_host.set_ptz_command.assert_called_with( + 0, sub_channel, command="Up", speed=5 + ) reolink_host.set_ptz_command.side_effect = ReolinkError("Test error") with pytest.raises(HomeAssistantError):