Add Reolink smart ai number entities (#140417)

This commit is contained in:
starkillerOG 2025-03-25 10:49:10 +01:00 committed by GitHub
parent 615afeb4d5
commit e7eb173e07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 341 additions and 4 deletions

View File

@ -217,6 +217,21 @@
"ai_animal_sensitivity": {
"default": "mdi:paw"
},
"crossline_sensitivity": {
"default": "mdi:fence"
},
"intrusion_sensitivity": {
"default": "mdi:location-enter"
},
"linger_sensitivity": {
"default": "mdi:account-switch"
},
"forgotten_item_sensitivity": {
"default": "mdi:package-variant-closed-plus"
},
"taken_item_sensitivity": {
"default": "mdi:package-variant-closed-minus"
},
"ai_face_delay": {
"default": "mdi:face-recognition"
},
@ -235,6 +250,18 @@
"ai_animal_delay": {
"default": "mdi:paw"
},
"intrusion_delay": {
"default": "mdi:location-enter"
},
"linger_delay": {
"default": "mdi:account-switch"
},
"forgotten_item_delay": {
"default": "mdi:package-variant-closed-plus"
},
"taken_item_delay": {
"default": "mdi:package-variant-closed-minus"
},
"auto_quick_reply_time": {
"default": "mdi:message-reply-text-outline"
},

View File

@ -9,6 +9,7 @@ from typing import Any
from reolink_aio.api import Chime, Host
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
@ -44,6 +45,19 @@ class ReolinkNumberEntityDescription(
value: Callable[[Host, int], float | None]
@dataclass(frozen=True, kw_only=True)
class ReolinkSmartAINumberEntityDescription(
NumberEntityDescription,
ReolinkChannelEntityDescription,
):
"""A class that describes smart AI number entities."""
smart_type: str
method: Callable[[Host, int, int, float], Any]
mode: NumberMode = NumberMode.AUTO
value: Callable[[Host, int, int], float | None]
@dataclass(frozen=True, kw_only=True)
class ReolinkHostNumberEntityDescription(
NumberEntityDescription,
@ -125,6 +139,7 @@ NUMBER_ENTITIES = (
cmd_key="GetPtzGuard",
translation_key="guard_return_time",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=10,
@ -248,6 +263,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_face_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -264,6 +280,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_person_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -280,6 +297,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_vehicle_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -296,6 +314,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_package_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -312,6 +331,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_pet_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -330,6 +350,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiAlarm",
translation_key="ai_animal_delay",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
entity_registry_enabled_default=False,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
@ -346,6 +367,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAutoReply",
translation_key="auto_quick_reply_time",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
@ -385,6 +407,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiCfg",
translation_key="auto_track_disappear_time",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
@ -400,6 +423,7 @@ NUMBER_ENTITIES = (
cmd_key="GetAiCfg",
translation_key="auto_track_stop_time",
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
@ -493,6 +517,168 @@ NUMBER_ENTITIES = (
),
)
SMART_AI_NUMBER_ENTITIES = (
ReolinkSmartAINumberEntityDescription(
key="crossline_sensitivity",
smart_type="crossline",
cmd_id=527,
translation_key="crossline_sensitivity",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ai_crossline"),
value=lambda api, ch, loc: (
api.baichuan.smart_ai_sensitivity(ch, "crossline", loc)
),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "crossline", loc, sensitivity=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="intrusion_sensitivity",
smart_type="intrusion",
cmd_id=529,
translation_key="intrusion_sensitivity",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ai_intrusion"),
value=lambda api, ch, loc: (
api.baichuan.smart_ai_sensitivity(ch, "intrusion", loc)
),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "intrusion", loc, sensitivity=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="linger_sensitivity",
smart_type="loitering",
cmd_id=531,
translation_key="linger_sensitivity",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ai_linger"),
value=lambda api, ch, loc: (
api.baichuan.smart_ai_sensitivity(ch, "loitering", loc)
),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "loitering", loc, sensitivity=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="forgotten_item_sensitivity",
smart_type="legacy",
cmd_id=549,
translation_key="forgotten_item_sensitivity",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ai_forgotten_item"),
value=lambda api, ch, loc: (
api.baichuan.smart_ai_sensitivity(ch, "legacy", loc)
),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "legacy", loc, sensitivity=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="taken_item_sensitivity",
smart_type="loss",
cmd_id=551,
translation_key="taken_item_sensitivity",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
native_step=1,
native_min_value=0,
native_max_value=100,
supported=lambda api, ch: api.supported(ch, "ai_taken_item"),
value=lambda api, ch, loc: api.baichuan.smart_ai_sensitivity(ch, "loss", loc),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "loss", loc, sensitivity=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="intrusion_delay",
smart_type="intrusion",
cmd_id=529,
translation_key="intrusion_delay",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=0,
native_max_value=10,
supported=lambda api, ch: api.supported(ch, "ai_intrusion"),
value=lambda api, ch, loc: api.baichuan.smart_ai_delay(ch, "intrusion", loc),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "intrusion", loc, delay=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="linger_delay",
smart_type="loitering",
cmd_id=531,
translation_key="linger_delay",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
native_max_value=10,
supported=lambda api, ch: api.supported(ch, "ai_linger"),
value=lambda api, ch, loc: api.baichuan.smart_ai_delay(ch, "loitering", loc),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "loitering", loc, delay=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="forgotten_item_delay",
smart_type="legacy",
cmd_id=549,
translation_key="forgotten_item_delay",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
native_max_value=30,
supported=lambda api, ch: api.supported(ch, "ai_forgotten_item"),
value=lambda api, ch, loc: api.baichuan.smart_ai_delay(ch, "legacy", loc),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "legacy", loc, delay=int(value)
),
),
ReolinkSmartAINumberEntityDescription(
key="taken_item_delay",
smart_type="loss",
cmd_id=551,
translation_key="taken_item_delay",
entity_registry_enabled_default=False,
entity_category=EntityCategory.CONFIG,
device_class=NumberDeviceClass.DURATION,
native_step=1,
native_unit_of_measurement=UnitOfTime.SECONDS,
native_min_value=1,
native_max_value=30,
supported=lambda api, ch: api.supported(ch, "ai_taken_item"),
value=lambda api, ch, loc: api.baichuan.smart_ai_delay(ch, "loss", loc),
method=lambda api, ch, loc, value: api.baichuan.set_smart_ai(
ch, "loss", loc, delay=int(value)
),
),
)
HOST_NUMBER_ENTITIES = (
ReolinkHostNumberEntityDescription(
key="alarm_volume",
@ -542,22 +728,32 @@ async def async_setup_entry(
) -> None:
"""Set up a Reolink number entities."""
reolink_data: ReolinkData = config_entry.runtime_data
api = reolink_data.host.api
entities: list[NumberEntity] = [
ReolinkNumberEntity(reolink_data, channel, entity_description)
for entity_description in NUMBER_ENTITIES
for channel in reolink_data.host.api.channels
if entity_description.supported(reolink_data.host.api, channel)
for channel in api.channels
if entity_description.supported(api, channel)
]
entities.extend(
ReolinkSmartAINumberEntity(reolink_data, channel, location, entity_description)
for entity_description in SMART_AI_NUMBER_ENTITIES
for channel in api.channels
for location in api.baichuan.smart_location_list(
channel, entity_description.smart_type
)
if entity_description.supported(api, channel)
)
entities.extend(
ReolinkHostNumberEntity(reolink_data, entity_description)
for entity_description in HOST_NUMBER_ENTITIES
if entity_description.supported(reolink_data.host.api)
if entity_description.supported(api)
)
entities.extend(
ReolinkChimeNumberEntity(reolink_data, chime, entity_description)
for entity_description in CHIME_NUMBER_ENTITIES
for chime in reolink_data.host.api.chime_list
for chime in api.chime_list
)
async_add_entities(entities)
@ -599,6 +795,51 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
self.async_write_ha_state()
class ReolinkSmartAINumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
"""Base smart AI number entity class for Reolink IP cameras."""
entity_description: ReolinkSmartAINumberEntityDescription
def __init__(
self,
reolink_data: ReolinkData,
channel: int,
location: int,
entity_description: ReolinkSmartAINumberEntityDescription,
) -> None:
"""Initialize Reolink number entity."""
self.entity_description = entity_description
super().__init__(reolink_data, channel)
unique_index = self._host.api.baichuan.smart_ai_index(
channel, entity_description.smart_type, location
)
self._attr_unique_id = f"{self._attr_unique_id}_{unique_index}"
self._location = location
self._attr_mode = entity_description.mode
self._attr_translation_placeholders = {
"zone_name": self._host.api.baichuan.smart_ai_name(
channel, entity_description.smart_type, location
)
}
@property
def native_value(self) -> float | None:
"""State of the number entity."""
return self.entity_description.value(
self._host.api, self._channel, self._location
)
@raise_translated_error
async def async_set_native_value(self, value: float) -> None:
"""Update the current value."""
await self.entity_description.method(
self._host.api, self._channel, self._location, value
)
self.async_write_ha_state()
class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity):
"""Base number entity class for Reolink Host."""

View File

@ -562,6 +562,21 @@
"ai_animal_sensitivity": {
"name": "AI animal sensitivity"
},
"crossline_sensitivity": {
"name": "AI crossline {zone_name} sensitivity"
},
"intrusion_sensitivity": {
"name": "AI intrusion {zone_name} sensitivity"
},
"linger_sensitivity": {
"name": "AI linger {zone_name} sensitivity"
},
"forgotten_item_sensitivity": {
"name": "AI item forgotten {zone_name} sensitivity"
},
"taken_item_sensitivity": {
"name": "AI item taken {zone_name} sensitivity"
},
"ai_face_delay": {
"name": "AI face delay"
},
@ -580,6 +595,18 @@
"ai_animal_delay": {
"name": "AI animal delay"
},
"intrusion_delay": {
"name": "AI intrusion {zone_name} delay"
},
"linger_delay": {
"name": "AI linger {zone_name} delay"
},
"forgotten_item_delay": {
"name": "AI item forgotten {zone_name} delay"
},
"taken_item_delay": {
"name": "AI item taken {zone_name} delay"
},
"auto_quick_reply_time": {
"name": "Auto quick reply time"
},

View File

@ -67,6 +67,48 @@ async def test_number(
reolink_connect.set_volume.reset_mock(side_effect=True)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_smart_ai_number(
hass: HomeAssistant,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
) -> None:
"""Test number entity with smart ai sensitivity."""
reolink_connect.baichuan.smart_ai_sensitivity.return_value = 80
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.NUMBER]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.NUMBER}.{TEST_NVR_NAME}_AI_crossline_zone1_sensitivity"
assert hass.states.get(entity_id).state == "80"
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50},
blocking=True,
)
reolink_connect.baichuan.set_smart_ai.assert_called_with(
0, "crossline", 0, sensitivity=50
)
reolink_connect.baichuan.set_smart_ai.side_effect = InvalidParameterError(
"Test error"
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 50},
blocking=True,
)
reolink_connect.baichuan.set_smart_ai.reset_mock(side_effect=True)
async def test_host_number(
hass: HomeAssistant,
config_entry: MockConfigEntry,