mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Add Reolink hub volume number entities (#126389)
* Add Home Hub alarm and message volume * fix styling * Add tests * Update homeassistant/components/reolink/number.py * Update test_diagnostics.ambr --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
286c22c0ed
commit
90957dfedb
@ -106,6 +106,18 @@
|
|||||||
"0": "mdi:volume-off"
|
"0": "mdi:volume-off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"alarm_volume": {
|
||||||
|
"default": "mdi:volume-high",
|
||||||
|
"state": {
|
||||||
|
"0": "mdi:volume-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message_volume": {
|
||||||
|
"default": "mdi:volume-high",
|
||||||
|
"state": {
|
||||||
|
"0": "mdi:volume-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
"guard_return_time": {
|
"guard_return_time": {
|
||||||
"default": "mdi:crosshairs-gps"
|
"default": "mdi:crosshairs-gps"
|
||||||
},
|
},
|
||||||
|
@ -24,6 +24,8 @@ from .entity import (
|
|||||||
ReolinkChannelEntityDescription,
|
ReolinkChannelEntityDescription,
|
||||||
ReolinkChimeCoordinatorEntity,
|
ReolinkChimeCoordinatorEntity,
|
||||||
ReolinkChimeEntityDescription,
|
ReolinkChimeEntityDescription,
|
||||||
|
ReolinkHostCoordinatorEntity,
|
||||||
|
ReolinkHostEntityDescription,
|
||||||
)
|
)
|
||||||
from .util import ReolinkConfigEntry, ReolinkData
|
from .util import ReolinkConfigEntry, ReolinkData
|
||||||
|
|
||||||
@ -42,6 +44,18 @@ class ReolinkNumberEntityDescription(
|
|||||||
value: Callable[[Host, int], float | None]
|
value: Callable[[Host, int], float | None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class ReolinkHostNumberEntityDescription(
|
||||||
|
NumberEntityDescription,
|
||||||
|
ReolinkHostEntityDescription,
|
||||||
|
):
|
||||||
|
"""A class that describes number entities for the host."""
|
||||||
|
|
||||||
|
method: Callable[[Host, float], Any]
|
||||||
|
mode: NumberMode = NumberMode.AUTO
|
||||||
|
value: Callable[[Host], float | None]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ReolinkChimeNumberEntityDescription(
|
class ReolinkChimeNumberEntityDescription(
|
||||||
NumberEntityDescription,
|
NumberEntityDescription,
|
||||||
@ -474,6 +488,33 @@ NUMBER_ENTITIES = (
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HOST_NUMBER_ENTITIES = (
|
||||||
|
ReolinkHostNumberEntityDescription(
|
||||||
|
key="alarm_volume",
|
||||||
|
cmd_key="GetDeviceAudioCfg",
|
||||||
|
translation_key="alarm_volume",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_step=1,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=100,
|
||||||
|
supported=lambda api: api.supported(None, "hub_audio"),
|
||||||
|
value=lambda api: api.alarm_volume,
|
||||||
|
method=lambda api, value: api.set_hub_audio(alarm_volume=int(value)),
|
||||||
|
),
|
||||||
|
ReolinkHostNumberEntityDescription(
|
||||||
|
key="message_volume",
|
||||||
|
cmd_key="GetDeviceAudioCfg",
|
||||||
|
translation_key="message_volume",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
native_step=1,
|
||||||
|
native_min_value=0,
|
||||||
|
native_max_value=100,
|
||||||
|
supported=lambda api: api.supported(None, "hub_audio"),
|
||||||
|
value=lambda api: api.message_volume,
|
||||||
|
method=lambda api, value: api.set_hub_audio(message_volume=int(value)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
CHIME_NUMBER_ENTITIES = (
|
CHIME_NUMBER_ENTITIES = (
|
||||||
ReolinkChimeNumberEntityDescription(
|
ReolinkChimeNumberEntityDescription(
|
||||||
key="volume",
|
key="volume",
|
||||||
@ -497,12 +538,17 @@ async def async_setup_entry(
|
|||||||
"""Set up a Reolink number entities."""
|
"""Set up a Reolink number entities."""
|
||||||
reolink_data: ReolinkData = config_entry.runtime_data
|
reolink_data: ReolinkData = config_entry.runtime_data
|
||||||
|
|
||||||
entities: list[ReolinkNumberEntity | ReolinkChimeNumberEntity] = [
|
entities: list[NumberEntity] = [
|
||||||
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(
|
||||||
|
ReolinkHostNumberEntity(reolink_data, entity_description)
|
||||||
|
for entity_description in HOST_NUMBER_ENTITIES
|
||||||
|
if entity_description.supported(reolink_data.host.api)
|
||||||
|
)
|
||||||
entities.extend(
|
entities.extend(
|
||||||
ReolinkChimeNumberEntity(reolink_data, chime, entity_description)
|
ReolinkChimeNumberEntity(reolink_data, chime, entity_description)
|
||||||
for entity_description in CHIME_NUMBER_ENTITIES
|
for entity_description in CHIME_NUMBER_ENTITIES
|
||||||
@ -552,6 +598,38 @@ class ReolinkNumberEntity(ReolinkChannelCoordinatorEntity, NumberEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
|
class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity):
|
||||||
|
"""Base number entity class for Reolink Host."""
|
||||||
|
|
||||||
|
entity_description: ReolinkHostNumberEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
reolink_data: ReolinkData,
|
||||||
|
entity_description: ReolinkHostNumberEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize Reolink number entity."""
|
||||||
|
self.entity_description = entity_description
|
||||||
|
super().__init__(reolink_data)
|
||||||
|
|
||||||
|
self._attr_mode = entity_description.mode
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | None:
|
||||||
|
"""State of the number entity."""
|
||||||
|
return self.entity_description.value(self._host.api)
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Update the current value."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.method(self._host.api, value)
|
||||||
|
except InvalidParameterError as err:
|
||||||
|
raise ServiceValidationError(err) from err
|
||||||
|
except ReolinkError as err:
|
||||||
|
raise HomeAssistantError(err) from err
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
|
||||||
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
|
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
|
||||||
"""Base number entity class for Reolink IP cameras."""
|
"""Base number entity class for Reolink IP cameras."""
|
||||||
|
|
||||||
|
@ -395,6 +395,12 @@
|
|||||||
"volume": {
|
"volume": {
|
||||||
"name": "Volume"
|
"name": "Volume"
|
||||||
},
|
},
|
||||||
|
"alarm_volume": {
|
||||||
|
"name": "Alarm volume"
|
||||||
|
},
|
||||||
|
"message_volume": {
|
||||||
|
"name": "Message volume"
|
||||||
|
},
|
||||||
"guard_return_time": {
|
"guard_return_time": {
|
||||||
"name": "Guard return time"
|
"name": "Guard return time"
|
||||||
},
|
},
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
}),
|
}),
|
||||||
'GetDeviceAudioCfg': dict({
|
'GetDeviceAudioCfg': dict({
|
||||||
'0': 2,
|
'0': 2,
|
||||||
'null': 2,
|
'null': 4,
|
||||||
}),
|
}),
|
||||||
'GetEmail': dict({
|
'GetEmail': dict({
|
||||||
'0': 1,
|
'0': 1,
|
||||||
|
@ -65,6 +65,50 @@ async def test_number(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_host_number(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
reolink_connect: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test number entity with volume."""
|
||||||
|
reolink_connect.alarm_volume = 85
|
||||||
|
|
||||||
|
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}_alarm_volume"
|
||||||
|
|
||||||
|
assert hass.states.get(entity_id).state == "85"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
NUMBER_DOMAIN,
|
||||||
|
SERVICE_SET_VALUE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 45},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
reolink_connect.set_hub_audio.assert_called_with(alarm_volume=45)
|
||||||
|
|
||||||
|
reolink_connect.set_hub_audio.side_effect = ReolinkError("Test error")
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
await hass.services.async_call(
|
||||||
|
NUMBER_DOMAIN,
|
||||||
|
SERVICE_SET_VALUE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 45},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
reolink_connect.set_hub_audio.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: 45},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_chime_number(
|
async def test_chime_number(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user