mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Add Reolink home hub scene select entity (#140823)
This commit is contained in:
parent
245f0a1958
commit
334359871d
@ -350,6 +350,9 @@
|
|||||||
},
|
},
|
||||||
"sub_bit_rate": {
|
"sub_bit_rate": {
|
||||||
"default": "mdi:play-speed"
|
"default": "mdi:play-speed"
|
||||||
|
},
|
||||||
|
"scene_mode": {
|
||||||
|
"default": "mdi:view-list"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
@ -30,6 +30,8 @@ from .entity import (
|
|||||||
ReolinkChannelEntityDescription,
|
ReolinkChannelEntityDescription,
|
||||||
ReolinkChimeCoordinatorEntity,
|
ReolinkChimeCoordinatorEntity,
|
||||||
ReolinkChimeEntityDescription,
|
ReolinkChimeEntityDescription,
|
||||||
|
ReolinkHostCoordinatorEntity,
|
||||||
|
ReolinkHostEntityDescription,
|
||||||
)
|
)
|
||||||
from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error
|
from .util import ReolinkConfigEntry, ReolinkData, raise_translated_error
|
||||||
|
|
||||||
@ -49,6 +51,18 @@ class ReolinkSelectEntityDescription(
|
|||||||
value: Callable[[Host, int], str] | None = None
|
value: Callable[[Host, int], str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ReolinkHostSelectEntityDescription(
|
||||||
|
SelectEntityDescription,
|
||||||
|
ReolinkHostEntityDescription,
|
||||||
|
):
|
||||||
|
"""A class that describes host select entities."""
|
||||||
|
|
||||||
|
get_options: Callable[[Host], list[str]]
|
||||||
|
method: Callable[[Host, str], Any]
|
||||||
|
value: Callable[[Host], str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ReolinkChimeSelectEntityDescription(
|
class ReolinkChimeSelectEntityDescription(
|
||||||
SelectEntityDescription,
|
SelectEntityDescription,
|
||||||
@ -238,6 +252,19 @@ SELECT_ENTITIES = (
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HOST_SELECT_ENTITIES = (
|
||||||
|
ReolinkHostSelectEntityDescription(
|
||||||
|
key="scene_mode",
|
||||||
|
cmd_key="GetScene",
|
||||||
|
translation_key="scene_mode",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
get_options=lambda api: api.baichuan.scene_names,
|
||||||
|
supported=lambda api: api.supported(None, "scenes"),
|
||||||
|
value=lambda api: api.baichuan.active_scene,
|
||||||
|
method=lambda api, name: api.baichuan.set_scene(scene_name=name),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
CHIME_SELECT_ENTITIES = (
|
CHIME_SELECT_ENTITIES = (
|
||||||
ReolinkChimeSelectEntityDescription(
|
ReolinkChimeSelectEntityDescription(
|
||||||
key="motion_tone",
|
key="motion_tone",
|
||||||
@ -300,12 +327,19 @@ async def async_setup_entry(
|
|||||||
"""Set up a Reolink select entities."""
|
"""Set up a Reolink select entities."""
|
||||||
reolink_data: ReolinkData = config_entry.runtime_data
|
reolink_data: ReolinkData = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[ReolinkSelectEntity | ReolinkChimeSelectEntity] = [
|
entities: list[
|
||||||
|
ReolinkSelectEntity | ReolinkHostSelectEntity | 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(
|
||||||
|
ReolinkHostSelectEntity(reolink_data, entity_description)
|
||||||
|
for entity_description in HOST_SELECT_ENTITIES
|
||||||
|
if entity_description.supported(reolink_data.host.api)
|
||||||
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
ReolinkChimeSelectEntity(reolink_data, chime, entity_description)
|
ReolinkChimeSelectEntity(reolink_data, chime, entity_description)
|
||||||
for entity_description in CHIME_SELECT_ENTITIES
|
for entity_description in CHIME_SELECT_ENTITIES
|
||||||
@ -360,6 +394,33 @@ class ReolinkSelectEntity(ReolinkChannelCoordinatorEntity, SelectEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkHostSelectEntity(ReolinkHostCoordinatorEntity, SelectEntity):
|
||||||
|
"""Base select entity class for Reolink Host."""
|
||||||
|
|
||||||
|
entity_description: ReolinkHostSelectEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
entity_description: ReolinkHostSelectEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Reolink select entity."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
super().__init__(reolink_data)
|
||||||
|
self._attr_options = entity_description.get_options(self._host.api)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_option(self) -> str | None:
|
||||||
|
"""Return the current option."""
|
||||||
|
return self.entity_description.value(self._host.api)
|
||||||
|
|
||||||
|
@raise_translated_error
|
||||||
|
async def async_select_option(self, option: str) -> None:
|
||||||
|
"""Change the selected option."""
|
||||||
|
await self.entity_description.method(self._host.api, option)
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||||
"""Base select entity class for Reolink IP cameras."""
|
"""Base select entity class for Reolink IP cameras."""
|
||||||
|
|
||||||
|
@ -799,6 +799,15 @@
|
|||||||
},
|
},
|
||||||
"sub_bit_rate": {
|
"sub_bit_rate": {
|
||||||
"name": "Fluent bit rate"
|
"name": "Fluent bit rate"
|
||||||
|
},
|
||||||
|
"scene_mode": {
|
||||||
|
"name": "Scene mode",
|
||||||
|
"state": {
|
||||||
|
"off": "[%key:common::state::off%]",
|
||||||
|
"disarm": "Disarmed",
|
||||||
|
"home": "Home",
|
||||||
|
"away": "Away"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
@ -145,6 +145,8 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
|||||||
host_mock.baichuan.privacy_mode.return_value = False
|
host_mock.baichuan.privacy_mode.return_value = False
|
||||||
host_mock.baichuan.day_night_state.return_value = "day"
|
host_mock.baichuan.day_night_state.return_value = "day"
|
||||||
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
||||||
|
host_mock.baichuan.active_scene = "off"
|
||||||
|
host_mock.baichuan.scene_names = ["off", "home"]
|
||||||
host_mock.baichuan.abilities = {
|
host_mock.baichuan.abilities = {
|
||||||
0: {"chnID": 0, "aitype": 34615},
|
0: {"chnID": 0, "aitype": 34615},
|
||||||
"Host": {"pushAlarm": 7},
|
"Host": {"pushAlarm": 7},
|
||||||
|
@ -170,6 +170,9 @@
|
|||||||
'0': 1,
|
'0': 1,
|
||||||
'null': 2,
|
'null': 2,
|
||||||
}),
|
}),
|
||||||
|
'GetScene': dict({
|
||||||
|
'null': 1,
|
||||||
|
}),
|
||||||
'GetStateLight': dict({
|
'GetStateLight': dict({
|
||||||
'null': 1,
|
'null': 1,
|
||||||
}),
|
}),
|
||||||
|
@ -104,6 +104,58 @@ async def test_play_quick_reply_message(
|
|||||||
reolink_connect.quick_reply_dict = MagicMock()
|
reolink_connect.quick_reply_dict = MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_host_scene_select(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test host select entity with scene mode."""
|
||||||
|
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()
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_scene_mode"
|
||||||
|
assert hass.states.get(entity_id).state == "off"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
SELECT_DOMAIN,
|
||||||
|
SERVICE_SELECT_OPTION,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, "option": "home"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
reolink_connect.baichuan.set_scene.assert_called_once()
|
||||||
|
|
||||||
|
reolink_connect.baichuan.set_scene.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": "home"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
reolink_connect.baichuan.set_scene.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": "home"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
reolink_connect.baichuan.active_scene = "Invalid value"
|
||||||
|
freezer.tick(DEVICE_UPDATE_INTERVAL)
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get(entity_id).state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
reolink_connect.baichuan.set_scene.reset_mock(side_effect=True)
|
||||||
|
reolink_connect.baichuan.active_scene = "off"
|
||||||
|
|
||||||
|
|
||||||
async def test_chime_select(
|
async def test_chime_select(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
freezer: FrozenDateTimeFactory,
|
freezer: FrozenDateTimeFactory,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user