Add Reolink chime support (#122752)
This commit is contained in:
parent
f764705629
commit
8c0d9a1320
13 changed files with 549 additions and 54 deletions
|
@ -186,7 +186,7 @@ async def async_remove_config_entry_device(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Remove a device from a config entry."""
|
"""Remove a device from a config entry."""
|
||||||
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
||||||
(device_uid, ch) = get_device_uid_and_ch(device, host)
|
(device_uid, ch, is_chime) = get_device_uid_and_ch(device, host)
|
||||||
|
|
||||||
if not host.api.is_nvr or ch is None:
|
if not host.api.is_nvr or ch is None:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
|
@ -227,20 +227,24 @@ async def async_remove_config_entry_device(
|
||||||
|
|
||||||
def get_device_uid_and_ch(
|
def get_device_uid_and_ch(
|
||||||
device: dr.DeviceEntry, host: ReolinkHost
|
device: dr.DeviceEntry, host: ReolinkHost
|
||||||
) -> tuple[list[str], int | None]:
|
) -> tuple[list[str], int | None, bool]:
|
||||||
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
|
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
|
||||||
device_uid = [
|
device_uid = [
|
||||||
dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN
|
dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN
|
||||||
][0]
|
][0]
|
||||||
|
|
||||||
|
is_chime = False
|
||||||
if len(device_uid) < 2:
|
if len(device_uid) < 2:
|
||||||
# NVR itself
|
# NVR itself
|
||||||
ch = None
|
ch = None
|
||||||
elif device_uid[1].startswith("ch") and len(device_uid[1]) <= 5:
|
elif device_uid[1].startswith("ch") and len(device_uid[1]) <= 5:
|
||||||
ch = int(device_uid[1][2:])
|
ch = int(device_uid[1][2:])
|
||||||
|
elif device_uid[1].startswith("chime"):
|
||||||
|
ch = int(device_uid[1][5:])
|
||||||
|
is_chime = True
|
||||||
else:
|
else:
|
||||||
ch = host.api.channel_for_uid(device_uid[1])
|
ch = host.api.channel_for_uid(device_uid[1])
|
||||||
return (device_uid, ch)
|
return (device_uid, ch, is_chime)
|
||||||
|
|
||||||
|
|
||||||
def migrate_entity_ids(
|
def migrate_entity_ids(
|
||||||
|
@ -251,7 +255,7 @@ def migrate_entity_ids(
|
||||||
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
||||||
ch_device_ids = {}
|
ch_device_ids = {}
|
||||||
for device in devices:
|
for device in devices:
|
||||||
(device_uid, ch) = get_device_uid_and_ch(device, host)
|
(device_uid, ch, is_chime) = get_device_uid_and_ch(device, host)
|
||||||
|
|
||||||
if host.api.supported(None, "UID") and device_uid[0] != host.unique_id:
|
if host.api.supported(None, "UID") and device_uid[0] != host.unique_id:
|
||||||
if ch is None:
|
if ch is None:
|
||||||
|
@ -261,8 +265,8 @@ def migrate_entity_ids(
|
||||||
new_identifiers = {(DOMAIN, new_device_id)}
|
new_identifiers = {(DOMAIN, new_device_id)}
|
||||||
device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
|
device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
|
||||||
|
|
||||||
if ch is None:
|
if ch is None or is_chime:
|
||||||
continue # Do not consider the NVR itself
|
continue # Do not consider the NVR itself or chimes
|
||||||
|
|
||||||
ch_device_ids[device.id] = ch
|
ch_device_ids[device.id] = ch
|
||||||
if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch):
|
if host.api.supported(ch, "UID") and device_uid[1] != host.api.camera_uid(ch):
|
||||||
|
|
|
@ -117,18 +117,14 @@ async def async_setup_entry(
|
||||||
entities: list[ReolinkBinarySensorEntity] = []
|
entities: list[ReolinkBinarySensorEntity] = []
|
||||||
for channel in reolink_data.host.api.channels:
|
for channel in reolink_data.host.api.channels:
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkPushBinarySensorEntity(reolink_data, channel, entity_description)
|
||||||
ReolinkPushBinarySensorEntity(reolink_data, channel, entity_description)
|
for entity_description in BINARY_PUSH_SENSORS
|
||||||
for entity_description in BINARY_PUSH_SENSORS
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkBinarySensorEntity(reolink_data, channel, entity_description)
|
||||||
ReolinkBinarySensorEntity(reolink_data, channel, entity_description)
|
for entity_description in BINARY_SENSORS
|
||||||
for entity_description in BINARY_SENSORS
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
|
@ -164,11 +164,9 @@ async def async_setup_entry(
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
]
|
]
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkHostButtonEntity(reolink_data, entity_description)
|
||||||
ReolinkHostButtonEntity(reolink_data, entity_description)
|
for entity_description in HOST_BUTTON_ENTITIES
|
||||||
for entity_description in HOST_BUTTON_ENTITIES
|
if entity_description.supported(reolink_data.host.api)
|
||||||
if entity_description.supported(reolink_data.host.api)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from reolink_aio.api import DUAL_LENS_MODELS, Host
|
from reolink_aio.api import DUAL_LENS_MODELS, Chime, Host
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
@ -59,8 +59,9 @@ class ReolinkHostCoordinatorEntity(CoordinatorEntity[DataUpdateCoordinator[None]
|
||||||
|
|
||||||
http_s = "https" if self._host.api.use_https else "http"
|
http_s = "https" if self._host.api.use_https else "http"
|
||||||
self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
|
self._conf_url = f"{http_s}://{self._host.api.host}:{self._host.api.port}"
|
||||||
|
self._dev_id = self._host.unique_id
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, self._host.unique_id)},
|
identifiers={(DOMAIN, self._dev_id)},
|
||||||
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
|
connections={(CONNECTION_NETWORK_MAC, self._host.api.mac_address)},
|
||||||
name=self._host.api.nvr_name,
|
name=self._host.api.nvr_name,
|
||||||
model=self._host.api.model,
|
model=self._host.api.model,
|
||||||
|
@ -126,12 +127,14 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||||
|
|
||||||
if self._host.api.is_nvr:
|
if self._host.api.is_nvr:
|
||||||
if self._host.api.supported(dev_ch, "UID"):
|
if self._host.api.supported(dev_ch, "UID"):
|
||||||
dev_id = f"{self._host.unique_id}_{self._host.api.camera_uid(dev_ch)}"
|
self._dev_id = (
|
||||||
|
f"{self._host.unique_id}_{self._host.api.camera_uid(dev_ch)}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
dev_id = f"{self._host.unique_id}_ch{dev_ch}"
|
self._dev_id = f"{self._host.unique_id}_ch{dev_ch}"
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, dev_id)},
|
identifiers={(DOMAIN, self._dev_id)},
|
||||||
via_device=(DOMAIN, self._host.unique_id),
|
via_device=(DOMAIN, self._host.unique_id),
|
||||||
name=self._host.api.camera_name(dev_ch),
|
name=self._host.api.camera_name(dev_ch),
|
||||||
model=self._host.api.camera_model(dev_ch),
|
model=self._host.api.camera_model(dev_ch),
|
||||||
|
@ -156,3 +159,34 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||||
self._host.async_unregister_update_cmd(cmd_key, self._channel)
|
self._host.async_unregister_update_cmd(cmd_key, self._channel)
|
||||||
|
|
||||||
await super().async_will_remove_from_hass()
|
await super().async_will_remove_from_hass()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
||||||
|
"""Parent class for Reolink chime entities connected."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
chime: Chime,
|
||||||
|
coordinator: DataUpdateCoordinator[None] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
|
||||||
|
super().__init__(reolink_data, chime.channel, coordinator)
|
||||||
|
|
||||||
|
self._chime = chime
|
||||||
|
|
||||||
|
self._attr_unique_id = (
|
||||||
|
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
|
||||||
|
)
|
||||||
|
cam_dev_id = self._dev_id
|
||||||
|
self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}"
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._dev_id)},
|
||||||
|
via_device=(DOMAIN, cam_dev_id),
|
||||||
|
name=chime.name,
|
||||||
|
model="Reolink Chime",
|
||||||
|
manufacturer=self._host.api.manufacturer,
|
||||||
|
serial_number=str(chime.dev_id),
|
||||||
|
configuration_url=self._conf_url,
|
||||||
|
)
|
||||||
|
|
|
@ -206,6 +206,15 @@
|
||||||
},
|
},
|
||||||
"hdr": {
|
"hdr": {
|
||||||
"default": "mdi:hdr"
|
"default": "mdi:hdr"
|
||||||
|
},
|
||||||
|
"motion_tone": {
|
||||||
|
"default": "mdi:music-note"
|
||||||
|
},
|
||||||
|
"people_tone": {
|
||||||
|
"default": "mdi:music-note"
|
||||||
|
},
|
||||||
|
"visitor_tone": {
|
||||||
|
"default": "mdi:music-note"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
@ -284,6 +293,9 @@
|
||||||
},
|
},
|
||||||
"pir_reduce_alarm": {
|
"pir_reduce_alarm": {
|
||||||
"default": "mdi:motion-sensor"
|
"default": "mdi:motion-sensor"
|
||||||
|
},
|
||||||
|
"led": {
|
||||||
|
"default": "mdi:lightning-bolt-circle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from reolink_aio.api import Host
|
from reolink_aio.api import Chime, Host
|
||||||
from reolink_aio.exceptions import InvalidParameterError, ReolinkError
|
from reolink_aio.exceptions import InvalidParameterError, ReolinkError
|
||||||
|
|
||||||
from homeassistant.components.number import (
|
from homeassistant.components.number import (
|
||||||
|
@ -22,7 +22,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import ReolinkData
|
from . import ReolinkData
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
from .entity import (
|
||||||
|
ReolinkChannelCoordinatorEntity,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
ReolinkChimeCoordinatorEntity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
@ -39,6 +43,18 @@ class ReolinkNumberEntityDescription(
|
||||||
value: Callable[[Host, int], float | None]
|
value: Callable[[Host, int], float | None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ReolinkChimeNumberEntityDescription(
|
||||||
|
NumberEntityDescription,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
):
|
||||||
|
"""A class that describes number entities for a chime."""
|
||||||
|
|
||||||
|
method: Callable[[Chime, float], Any]
|
||||||
|
mode: NumberMode = NumberMode.AUTO
|
||||||
|
value: Callable[[Chime], float | None]
|
||||||
|
|
||||||
|
|
||||||
NUMBER_ENTITIES = (
|
NUMBER_ENTITIES = (
|
||||||
ReolinkNumberEntityDescription(
|
ReolinkNumberEntityDescription(
|
||||||
key="zoom",
|
key="zoom",
|
||||||
|
@ -459,6 +475,20 @@ NUMBER_ENTITIES = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CHIME_NUMBER_ENTITIES = (
|
||||||
|
ReolinkChimeNumberEntityDescription(
|
||||||
|
key="volume",
|
||||||
|
cmd_key="DingDongOpt",
|
||||||
|
translation_key="volume",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_step=1,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=4,
|
||||||
|
value=lambda chime: chime.volume,
|
||||||
|
method=lambda chime, value: chime.set_option(volume=int(value)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -468,12 +498,18 @@ async def async_setup_entry(
|
||||||
"""Set up a Reolink number entities."""
|
"""Set up a Reolink number entities."""
|
||||||
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
async_add_entities(
|
entities: list[ReolinkNumberEntity | ReolinkChimeNumberEntity] = [
|
||||||
ReolinkNumberEntity(reolink_data, channel, entity_description)
|
ReolinkNumberEntity(reolink_data, channel, entity_description)
|
||||||
for entity_description in NUMBER_ENTITIES
|
for entity_description in NUMBER_ENTITIES
|
||||||
for channel in reolink_data.host.api.channels
|
for channel in reolink_data.host.api.channels
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
|
]
|
||||||
|
entities.extend(
|
||||||
|
ReolinkChimeNumberEntity(reolink_data, chime, entity_description)
|
||||||
|
for entity_description in CHIME_NUMBER_ENTITIES
|
||||||
|
for chime in reolink_data.host.api.chime_list
|
||||||
)
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
||||||
|
@ -515,3 +551,36 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
raise HomeAssistantError(err) from err
|
raise HomeAssistantError(err) from err
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
|
||||||
|
"""Base number entity class for Reolink IP cameras."""
|
||||||
|
|
||||||
|
entity_description: ReolinkChimeNumberEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
chime: Chime,
|
||||||
|
entity_description: ReolinkChimeNumberEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Reolink chime number entity."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
super().__init__(reolink_data, chime)
|
||||||
|
|
||||||
|
self._attr_mode = entity_description.mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""State of the number entity."""
|
||||||
|
return self.entity_description.value(self._chime)
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Update the current value."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.method(self._chime, value)
|
||||||
|
except InvalidParameterError as err:
|
||||||
|
raise ServiceValidationError(err) from err
|
||||||
|
except ReolinkError as err:
|
||||||
|
raise HomeAssistantError(err) from err
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -8,6 +8,8 @@ import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from reolink_aio.api import (
|
from reolink_aio.api import (
|
||||||
|
Chime,
|
||||||
|
ChimeToneEnum,
|
||||||
DayNightEnum,
|
DayNightEnum,
|
||||||
HDREnum,
|
HDREnum,
|
||||||
Host,
|
Host,
|
||||||
|
@ -26,7 +28,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import ReolinkData
|
from . import ReolinkData
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
from .entity import (
|
||||||
|
ReolinkChannelCoordinatorEntity,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
ReolinkChimeCoordinatorEntity,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -43,6 +49,18 @@ class ReolinkSelectEntityDescription(
|
||||||
value: Callable[[Host, int], str] | None = None
|
value: Callable[[Host, int], str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ReolinkChimeSelectEntityDescription(
|
||||||
|
SelectEntityDescription,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
):
|
||||||
|
"""A class that describes select entities for a chime."""
|
||||||
|
|
||||||
|
get_options: list[str]
|
||||||
|
method: Callable[[Chime, str], Any]
|
||||||
|
value: Callable[[Chime], str]
|
||||||
|
|
||||||
|
|
||||||
def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
|
def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
|
||||||
"""Get the quick reply file id from the message string."""
|
"""Get the quick reply file id from the message string."""
|
||||||
return [k for k, v in api.quick_reply_dict(ch).items() if v == mess][0]
|
return [k for k, v in api.quick_reply_dict(ch).items() if v == mess][0]
|
||||||
|
@ -132,6 +150,36 @@ SELECT_ENTITIES = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CHIME_SELECT_ENTITIES = (
|
||||||
|
ReolinkChimeSelectEntityDescription(
|
||||||
|
key="motion_tone",
|
||||||
|
cmd_key="GetDingDongCfg",
|
||||||
|
translation_key="motion_tone",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
get_options=[method.name for method in ChimeToneEnum],
|
||||||
|
value=lambda chime: ChimeToneEnum(chime.tone("md")).name,
|
||||||
|
method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value),
|
||||||
|
),
|
||||||
|
ReolinkChimeSelectEntityDescription(
|
||||||
|
key="people_tone",
|
||||||
|
cmd_key="GetDingDongCfg",
|
||||||
|
translation_key="people_tone",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
get_options=[method.name for method in ChimeToneEnum],
|
||||||
|
value=lambda chime: ChimeToneEnum(chime.tone("people")).name,
|
||||||
|
method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
|
||||||
|
),
|
||||||
|
ReolinkChimeSelectEntityDescription(
|
||||||
|
key="visitor_tone",
|
||||||
|
cmd_key="GetDingDongCfg",
|
||||||
|
translation_key="visitor_tone",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
get_options=[method.name for method in ChimeToneEnum],
|
||||||
|
value=lambda chime: ChimeToneEnum(chime.tone("visitor")).name,
|
||||||
|
method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -141,12 +189,18 @@ async def async_setup_entry(
|
||||||
"""Set up a Reolink select entities."""
|
"""Set up a Reolink select entities."""
|
||||||
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
async_add_entities(
|
entities: list[ReolinkSelectEntity | ReolinkChimeSelectEntity] = [
|
||||||
ReolinkSelectEntity(reolink_data, channel, entity_description)
|
ReolinkSelectEntity(reolink_data, channel, entity_description)
|
||||||
for entity_description in SELECT_ENTITIES
|
for entity_description in SELECT_ENTITIES
|
||||||
for channel in reolink_data.host.api.channels
|
for channel in reolink_data.host.api.channels
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
|
]
|
||||||
|
entities.extend(
|
||||||
|
ReolinkChimeSelectEntity(reolink_data, chime, entity_description)
|
||||||
|
for entity_description in CHIME_SELECT_ENTITIES
|
||||||
|
for chime in reolink_data.host.api.chime_list
|
||||||
)
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
|
class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
|
||||||
|
@ -196,3 +250,45 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
|
||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
raise HomeAssistantError(err) from err
|
raise HomeAssistantError(err) from err
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||||
|
"""Base select entity class for Reolink IP cameras."""
|
||||||
|
|
||||||
|
entity_description: ReolinkChimeSelectEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
chime: Chime,
|
||||||
|
entity_description: ReolinkChimeSelectEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Reolink select entity for a chime."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
super().__init__(reolink_data, chime)
|
||||||
|
self._log_error = True
|
||||||
|
self._attr_options = entity_description.get_options
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the current option."""
|
||||||
|
try:
|
||||||
|
option = self.entity_description.value(self._chime)
|
||||||
|
except ValueError:
|
||||||
|
if self._log_error:
|
||||||
|
_LOGGER.exception("Reolink '%s' has an unknown value", self.name)
|
||||||
|
self._log_error = False
|
||||||
|
return None
|
||||||
|
|
||||||
|
self._log_error = True
|
||||||
|
return option
|
||||||
|
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.method(self._chime, option)
|
||||||
|
except InvalidParameterError as err:
|
||||||
|
raise ServiceValidationError(err) from err
|
||||||
|
except ReolinkError as err:
|
||||||
|
raise HomeAssistantError(err) from err
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -141,19 +141,15 @@ async def async_setup_entry(
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
]
|
]
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkHostSensorEntity(reolink_data, entity_description)
|
||||||
ReolinkHostSensorEntity(reolink_data, entity_description)
|
for entity_description in HOST_SENSORS
|
||||||
for entity_description in HOST_SENSORS
|
if entity_description.supported(reolink_data.host.api)
|
||||||
if entity_description.supported(reolink_data.host.api)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkHddSensorEntity(reolink_data, hdd_index, entity_description)
|
||||||
ReolinkHddSensorEntity(reolink_data, hdd_index, entity_description)
|
for entity_description in HDD_SENSORS
|
||||||
for entity_description in HDD_SENSORS
|
for hdd_index in reolink_data.host.api.hdd_list
|
||||||
for hdd_index in reolink_data.host.api.hdd_list
|
if entity_description.supported(reolink_data.host.api, hdd_index)
|
||||||
if entity_description.supported(reolink_data.host.api, hdd_index)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
|
@ -491,6 +491,54 @@
|
||||||
"on": "[%key:common::state::on%]",
|
"on": "[%key:common::state::on%]",
|
||||||
"auto": "Auto"
|
"auto": "Auto"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"motion_tone": {
|
||||||
|
"name": "Motion ringtone",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"citybird": "City bird",
|
||||||
|
"originaltune": "Original tune",
|
||||||
|
"pianokey": "Piano key",
|
||||||
|
"loop": "Loop",
|
||||||
|
"attraction": "Attraction",
|
||||||
|
"hophop": "Hop hop",
|
||||||
|
"goodday": "Good day",
|
||||||
|
"operetta": "Operetta",
|
||||||
|
"moonlight": "Moonlight",
|
||||||
|
"waybackhome": "Way back home"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"people_tone": {
|
||||||
|
"name": "Person ringtone",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]",
|
||||||
|
"originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]",
|
||||||
|
"pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]",
|
||||||
|
"loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]",
|
||||||
|
"attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]",
|
||||||
|
"hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]",
|
||||||
|
"goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]",
|
||||||
|
"operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]",
|
||||||
|
"moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]",
|
||||||
|
"waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"visitor_tone": {
|
||||||
|
"name": "Visitor ringtone",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"citybird": "[%key:component::reolink::entity::select::motion_tone::state::citybird%]",
|
||||||
|
"originaltune": "[%key:component::reolink::entity::select::motion_tone::state::originaltune%]",
|
||||||
|
"pianokey": "[%key:component::reolink::entity::select::motion_tone::state::pianokey%]",
|
||||||
|
"loop": "[%key:component::reolink::entity::select::motion_tone::state::loop%]",
|
||||||
|
"attraction": "[%key:component::reolink::entity::select::motion_tone::state::attraction%]",
|
||||||
|
"hophop": "[%key:component::reolink::entity::select::motion_tone::state::hophop%]",
|
||||||
|
"goodday": "[%key:component::reolink::entity::select::motion_tone::state::goodday%]",
|
||||||
|
"operetta": "[%key:component::reolink::entity::select::motion_tone::state::operetta%]",
|
||||||
|
"moonlight": "[%key:component::reolink::entity::select::motion_tone::state::moonlight%]",
|
||||||
|
"waybackhome": "[%key:component::reolink::entity::select::motion_tone::state::waybackhome%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
@ -574,6 +622,9 @@
|
||||||
},
|
},
|
||||||
"pir_reduce_alarm": {
|
"pir_reduce_alarm": {
|
||||||
"name": "PIR reduce false alarm"
|
"name": "PIR reduce false alarm"
|
||||||
|
},
|
||||||
|
"led": {
|
||||||
|
"name": "LED"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from reolink_aio.api import Host
|
from reolink_aio.api import Chime, Host
|
||||||
from reolink_aio.exceptions import ReolinkError
|
from reolink_aio.exceptions import ReolinkError
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
@ -22,6 +22,7 @@ from .const import DOMAIN
|
||||||
from .entity import (
|
from .entity import (
|
||||||
ReolinkChannelCoordinatorEntity,
|
ReolinkChannelCoordinatorEntity,
|
||||||
ReolinkChannelEntityDescription,
|
ReolinkChannelEntityDescription,
|
||||||
|
ReolinkChimeCoordinatorEntity,
|
||||||
ReolinkHostCoordinatorEntity,
|
ReolinkHostCoordinatorEntity,
|
||||||
ReolinkHostEntityDescription,
|
ReolinkHostEntityDescription,
|
||||||
)
|
)
|
||||||
|
@ -49,6 +50,17 @@ class ReolinkNVRSwitchEntityDescription(
|
||||||
value: Callable[[Host], bool]
|
value: Callable[[Host], bool]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ReolinkChimeSwitchEntityDescription(
|
||||||
|
SwitchEntityDescription,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
):
|
||||||
|
"""A class that describes switch entities for a chime."""
|
||||||
|
|
||||||
|
method: Callable[[Chime, bool], Any]
|
||||||
|
value: Callable[[Chime], bool | None]
|
||||||
|
|
||||||
|
|
||||||
SWITCH_ENTITIES = (
|
SWITCH_ENTITIES = (
|
||||||
ReolinkSwitchEntityDescription(
|
ReolinkSwitchEntityDescription(
|
||||||
key="ir_lights",
|
key="ir_lights",
|
||||||
|
@ -245,6 +257,17 @@ NVR_SWITCH_ENTITIES = (
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CHIME_SWITCH_ENTITIES = (
|
||||||
|
ReolinkChimeSwitchEntityDescription(
|
||||||
|
key="chime_led",
|
||||||
|
cmd_key="DingDongOpt",
|
||||||
|
translation_key="led",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
value=lambda chime: chime.led_state,
|
||||||
|
method=lambda chime, value: chime.set_option(led=value),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Can be removed in HA 2025.2.0
|
# Can be removed in HA 2025.2.0
|
||||||
DEPRECATED_HDR = ReolinkSwitchEntityDescription(
|
DEPRECATED_HDR = ReolinkSwitchEntityDescription(
|
||||||
key="hdr",
|
key="hdr",
|
||||||
|
@ -266,18 +289,23 @@ async def async_setup_entry(
|
||||||
"""Set up a Reolink switch entities."""
|
"""Set up a Reolink switch entities."""
|
||||||
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
reolink_data: ReolinkData = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
entities: list[ReolinkSwitchEntity | ReolinkNVRSwitchEntity] = [
|
entities: list[
|
||||||
|
ReolinkSwitchEntity | ReolinkNVRSwitchEntity | ReolinkChimeSwitchEntity
|
||||||
|
] = [
|
||||||
ReolinkSwitchEntity(reolink_data, channel, entity_description)
|
ReolinkSwitchEntity(reolink_data, channel, entity_description)
|
||||||
for entity_description in SWITCH_ENTITIES
|
for entity_description in SWITCH_ENTITIES
|
||||||
for channel in reolink_data.host.api.channels
|
for channel in reolink_data.host.api.channels
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
]
|
]
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkNVRSwitchEntity(reolink_data, entity_description)
|
||||||
ReolinkNVRSwitchEntity(reolink_data, entity_description)
|
for entity_description in NVR_SWITCH_ENTITIES
|
||||||
for entity_description in NVR_SWITCH_ENTITIES
|
if entity_description.supported(reolink_data.host.api)
|
||||||
if entity_description.supported(reolink_data.host.api)
|
)
|
||||||
]
|
entities.extend(
|
||||||
|
ReolinkChimeSwitchEntity(reolink_data, chime, entity_description)
|
||||||
|
for entity_description in CHIME_SWITCH_ENTITIES
|
||||||
|
for chime in reolink_data.host.api.chime_list
|
||||||
)
|
)
|
||||||
|
|
||||||
# Can be removed in HA 2025.2.0
|
# Can be removed in HA 2025.2.0
|
||||||
|
@ -378,3 +406,40 @@ class ReolinkNVRSwitchEntity(ReolinkHostCoordinatorEntity, SwitchEntity):
|
||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
raise HomeAssistantError(err) from err
|
raise HomeAssistantError(err) from err
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity):
|
||||||
|
"""Base switch entity class for a chime."""
|
||||||
|
|
||||||
|
entity_description: ReolinkChimeSwitchEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
chime: Chime,
|
||||||
|
entity_description: ReolinkChimeSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Reolink switch entity."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
super().__init__(reolink_data, chime)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if switch is on."""
|
||||||
|
return self.entity_description.value(self._chime)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity on."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.method(self._chime, True)
|
||||||
|
except ReolinkError as err:
|
||||||
|
raise HomeAssistantError(err) from err
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the entity off."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.method(self._chime, False)
|
||||||
|
except ReolinkError as err:
|
||||||
|
raise HomeAssistantError(err) from err
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
|
@ -81,11 +81,9 @@ async def async_setup_entry(
|
||||||
if entity_description.supported(reolink_data.host.api, channel)
|
if entity_description.supported(reolink_data.host.api, channel)
|
||||||
]
|
]
|
||||||
entities.extend(
|
entities.extend(
|
||||||
[
|
ReolinkHostUpdateEntity(reolink_data, entity_description)
|
||||||
ReolinkHostUpdateEntity(reolink_data, entity_description)
|
for entity_description in HOST_UPDATE_ENTITIES
|
||||||
for entity_description in HOST_UPDATE_ENTITIES
|
if entity_description.supported(reolink_data.host.api)
|
||||||
if entity_description.supported(reolink_data.host.api)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
|
@ -282,6 +282,15 @@ async def test_removing_disconnected_cams(
|
||||||
True,
|
True,
|
||||||
False,
|
False,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
f"{TEST_MAC}_chime123456789_play_ringtone",
|
||||||
|
f"{TEST_UID}_chime123456789_play_ringtone",
|
||||||
|
f"{TEST_MAC}_chime123456789",
|
||||||
|
f"{TEST_UID}_chime123456789",
|
||||||
|
Platform.SELECT,
|
||||||
|
True,
|
||||||
|
False,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
f"{TEST_MAC}_0_record_audio",
|
f"{TEST_MAC}_0_record_audio",
|
||||||
f"{TEST_MAC}_{TEST_UID_CAM}_record_audio",
|
f"{TEST_MAC}_{TEST_UID_CAM}_record_audio",
|
||||||
|
|
167
tests/components/reolink/test_select.py
Normal file
167
tests/components/reolink/test_select.py
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
"""Test the Reolink select platform."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from reolink_aio.api import Chime
|
||||||
|
from reolink_aio.exceptions import InvalidParameterError, ReolinkError
|
||||||
|
|
||||||
|
from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL
|
||||||
|
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
|
from .conftest import TEST_NVR_NAME
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
|
||||||
|
|
||||||
|
async def test_floodlight_mode_select(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test select entity with floodlight_mode."""
|
||||||
|
reolink_connect.whiteled_mode.return_value = 1
|
||||||
|
reolink_connect.whiteled_mode_list.return_value = ["off", "auto"]
|
||||||
|
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode"
|
||||||
|
assert hass.states.is_state(entity_id, "auto")
|
||||||
|
|
||||||
|
reolink_connect.set_whiteled = AsyncMock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
reolink_connect.set_whiteled.assert_called_once()
|
||||||
|
|
||||||
|
reolink_connect.set_whiteled = AsyncMock(side_effect=ReolinkError("Test error"))
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
reolink_connect.set_whiteled = AsyncMock(
|
||||||
|
side_effect=InvalidParameterError("Test error")
|
||||||
|
)
|
||||||
|
with pytest.raises(ServiceValidationError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_play_quick_reply_message(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test select play_quick_reply_message entity."""
|
||||||
|
reolink_connect.quick_reply_dict.return_value = {0: "off", 1: "test message"}
|
||||||
|
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message"
|
||||||
|
assert hass.states.is_state(entity_id, STATE_UNKNOWN)
|
||||||
|
|
||||||
|
reolink_connect.play_quick_reply = AsyncMock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "test message"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
reolink_connect.play_quick_reply.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_chime_select(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test chime select entity."""
|
||||||
|
TEST_CHIME = Chime(
|
||||||
|
host=reolink_connect,
|
||||||
|
dev_id=12345678,
|
||||||
|
channel=0,
|
||||||
|
name="Test chime",
|
||||||
|
event_info={
|
||||||
|
"md": {"switch": 0, "musicId": 0},
|
||||||
|
"people": {"switch": 0, "musicId": 1},
|
||||||
|
"visitor": {"switch": 1, "musicId": 2},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
TEST_CHIME.volume = 3
|
||||||
|
TEST_CHIME.led_state = True
|
||||||
|
|
||||||
|
reolink_connect.chime_list = [TEST_CHIME]
|
||||||
|
|
||||||
|
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id) is True
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone"
|
||||||
|
assert hass.states.is_state(entity_id, "pianokey")
|
||||||
|
|
||||||
|
TEST_CHIME.set_tone = AsyncMock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
TEST_CHIME.set_tone.assert_called_once()
|
||||||
|
|
||||||
|
TEST_CHIME.set_tone = AsyncMock(side_effect=ReolinkError("Test error"))
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_CHIME.set_tone = AsyncMock(side_effect=InvalidParameterError("Test error"))
|
||||||
|
with pytest.raises(ServiceValidationError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "off"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_CHIME.event_info = {}
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.is_state(entity_id, STATE_UNKNOWN)
|
Loading…
Add table
Add a link
Reference in a new issue