mirror of
https://github.com/home-assistant/core.git
synced 2026-09-05 02:25:07 -05:00
Add support for Reolink chime connected to Home Hub (#151199)
This commit is contained in:
@@ -243,8 +243,45 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
|
||||
class ReolinkHostChimeCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
"""Parent class for Reolink chime entities connected to a Host."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
chime: Chime,
|
||||
coordinator: DataUpdateCoordinator[None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
|
||||
super().__init__(reolink_data, coordinator)
|
||||
self._channel = chime.channel
|
||||
self._chime = chime
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
|
||||
)
|
||||
via_dev_id = self._host.unique_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, via_dev_id),
|
||||
name=chime.name,
|
||||
model="Reolink Chime",
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
sw_version=chime.sw_version,
|
||||
serial_number=str(chime.dev_id),
|
||||
configuration_url=self._conf_url,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._chime.online
|
||||
|
||||
|
||||
class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
||||
"""Parent class for Reolink chime entities connected."""
|
||||
"""Parent class for Reolink chime entities connected through a camera."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -255,21 +292,21 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
||||
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
|
||||
assert chime.channel is not None
|
||||
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
|
||||
via_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),
|
||||
via_device=(DOMAIN, via_dev_id),
|
||||
name=chime.name,
|
||||
model="Reolink Chime",
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
sw_version=chime.sw_version,
|
||||
serial_number=str(chime.dev_id),
|
||||
configuration_url=self._conf_url,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -855,6 +856,12 @@ async def async_setup_entry(
|
||||
for chime in api.chime_list
|
||||
if chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeNumberEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_NUMBER_ENTITIES
|
||||
for chime in api.chime_list
|
||||
if chime.channel is None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -969,7 +976,36 @@ class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity):
|
||||
|
||||
|
||||
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
|
||||
"""Base number entity class for Reolink IP cameras."""
|
||||
"""Base number entity class for Reolink chimes connected through a camera."""
|
||||
|
||||
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)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
await self.entity_description.method(self._chime, value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeNumberEntity(ReolinkHostChimeCoordinatorEntity, NumberEntity):
|
||||
"""Base number entity class for Reolink chimes connected to the host."""
|
||||
|
||||
entity_description: ReolinkChimeNumberEntityDescription
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -73,7 +74,7 @@ class ReolinkChimeSelectEntityDescription(
|
||||
|
||||
get_options: list[str]
|
||||
method: Callable[[Chime, str], Any]
|
||||
value: Callable[[Chime], str]
|
||||
value: Callable[[Chime], str | None]
|
||||
|
||||
|
||||
def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
|
||||
@@ -332,7 +333,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda chime: "md" in chime.chime_event_types,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("md")).name,
|
||||
value=lambda chime: chime.tone_name("md"),
|
||||
method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -342,7 +343,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "people" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("people")).name,
|
||||
value=lambda chime: chime.tone_name("people"),
|
||||
method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -352,7 +353,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "vehicle" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("vehicle")).name,
|
||||
value=lambda chime: chime.tone_name("vehicle"),
|
||||
method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -362,7 +363,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "visitor" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("visitor")).name,
|
||||
value=lambda chime: chime.tone_name("visitor"),
|
||||
method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -372,7 +373,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "package" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("package")).name,
|
||||
value=lambda chime: chime.tone_name("package"),
|
||||
method=lambda chime, name: chime.set_tone("package", ChimeToneEnum[name].value),
|
||||
),
|
||||
)
|
||||
@@ -386,9 +387,7 @@ async def async_setup_entry(
|
||||
"""Set up a Reolink select entities."""
|
||||
reolink_data: ReolinkData = config_entry.runtime_data
|
||||
|
||||
entities: list[
|
||||
ReolinkSelectEntity | ReolinkHostSelectEntity | ReolinkChimeSelectEntity
|
||||
] = [
|
||||
entities: list[SelectEntity] = [
|
||||
ReolinkSelectEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in SELECT_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
@@ -405,6 +404,12 @@ async def async_setup_entry(
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if entity_description.supported(chime) and chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeSelectEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_SELECT_ENTITIES
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if entity_description.supported(chime) and chime.channel is None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -481,7 +486,7 @@ class ReolinkHostSelectEntity(ReolinkHostCoordinatorEntity, SelectEntity):
|
||||
|
||||
|
||||
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||
"""Base select entity class for Reolink IP cameras."""
|
||||
"""Base select entity class for Reolink chimes connected through a camera."""
|
||||
|
||||
entity_description: ReolinkChimeSelectEntityDescription
|
||||
|
||||
@@ -494,22 +499,40 @@ class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||
"""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, KeyError):
|
||||
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
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.method(self._chime, option)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeSelectEntity(ReolinkHostChimeCoordinatorEntity, SelectEntity):
|
||||
"""Base select entity class for Reolink chimes connected to a host."""
|
||||
|
||||
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._attr_options = entity_description.get_options
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current option."""
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
|
||||
@@ -20,6 +20,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -364,9 +365,7 @@ async def async_setup_entry(
|
||||
"""Set up a Reolink switch entities."""
|
||||
reolink_data: ReolinkData = config_entry.runtime_data
|
||||
|
||||
entities: list[
|
||||
ReolinkSwitchEntity | ReolinkNVRSwitchEntity | ReolinkChimeSwitchEntity
|
||||
] = [
|
||||
entities: list[SwitchEntity] = [
|
||||
ReolinkSwitchEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in SWITCH_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
@@ -383,6 +382,12 @@ async def async_setup_entry(
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeSwitchEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_SWITCH_ENTITIES
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if chime.channel is None
|
||||
)
|
||||
|
||||
# Can be removed in HA 2025.4.0
|
||||
depricated_dict = {}
|
||||
@@ -511,3 +516,36 @@ class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity):
|
||||
"""Turn the entity off."""
|
||||
await self.entity_description.method(self._chime, False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeSwitchEntity(ReolinkHostChimeCoordinatorEntity, 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)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.entity_description.method(self._chime, True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@raise_translated_error
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.entity_description.method(self._chime, False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -252,6 +252,7 @@ def reolink_chime(reolink_host: MagicMock) -> None:
|
||||
}
|
||||
TEST_CHIME.remove = AsyncMock()
|
||||
TEST_CHIME.set_option = AsyncMock()
|
||||
TEST_CHIME.update_enums()
|
||||
|
||||
reolink_host.chime_list = [TEST_CHIME]
|
||||
reolink_host.chime.return_value = TEST_CHIME
|
||||
|
||||
@@ -147,13 +147,16 @@ async def test_host_number(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel", [0, None])
|
||||
async def test_chime_number(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
channel: int | None,
|
||||
) -> None:
|
||||
"""Test number entity of a chime with chime volume."""
|
||||
reolink_chime.channel = channel
|
||||
reolink_chime.volume = 3
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.NUMBER]):
|
||||
|
||||
@@ -149,6 +149,7 @@ async def test_host_scene_select(
|
||||
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel", [0, None])
|
||||
async def test_chime_select(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@@ -156,8 +157,11 @@ async def test_chime_select(
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
entity_registry: er.EntityRegistry,
|
||||
channel: int | None,
|
||||
) -> None:
|
||||
"""Test chime select entity."""
|
||||
reolink_chime.channel = channel
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -197,6 +201,7 @@ async def test_chime_select(
|
||||
|
||||
# Test unavailable
|
||||
reolink_chime.event_info = {}
|
||||
reolink_chime.update_enums()
|
||||
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -164,14 +164,18 @@ async def test_host_switch(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("channel", [0, None])
|
||||
async def test_chime_switch(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
reolink_host: MagicMock,
|
||||
reolink_chime: Chime,
|
||||
channel: int | None,
|
||||
) -> None:
|
||||
"""Test host switch entity."""
|
||||
reolink_chime.channel = channel
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
Reference in New Issue
Block a user