Skip to content

Commit

Permalink
Add Reolink hub volume number entities (#126389)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
starkillerOG and joostlek authored Sep 22, 2024
1 parent 286c22c commit 90957df
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 2 deletions.
12 changes: 12 additions & 0 deletions homeassistant/components/reolink/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@
"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": {
"default": "mdi:crosshairs-gps"
},
Expand Down
80 changes: 79 additions & 1 deletion homeassistant/components/reolink/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
ReolinkChannelEntityDescription,
ReolinkChimeCoordinatorEntity,
ReolinkChimeEntityDescription,
ReolinkHostCoordinatorEntity,
ReolinkHostEntityDescription,
)
from .util import ReolinkConfigEntry, ReolinkData

Expand All @@ -42,6 +44,18 @@ class ReolinkNumberEntityDescription(
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)
class ReolinkChimeNumberEntityDescription(
NumberEntityDescription,
Expand Down Expand Up @@ -474,6 +488,33 @@ class ReolinkChimeNumberEntityDescription(
),
)

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 = (
ReolinkChimeNumberEntityDescription(
key="volume",
Expand All @@ -497,12 +538,17 @@ async def async_setup_entry(
"""Set up a Reolink number entities."""
reolink_data: ReolinkData = config_entry.runtime_data

entities: list[ReolinkNumberEntity | ReolinkChimeNumberEntity] = [
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)
]
entities.extend(
ReolinkHostNumberEntity(reolink_data, entity_description)
for entity_description in HOST_NUMBER_ENTITIES
if entity_description.supported(reolink_data.host.api)
)
entities.extend(
ReolinkChimeNumberEntity(reolink_data, chime, entity_description)
for entity_description in CHIME_NUMBER_ENTITIES
Expand Down Expand Up @@ -552,6 +598,38 @@ async def async_set_native_value(self, value: float) -> None:
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):
"""Base number entity class for Reolink IP cameras."""

Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/reolink/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,12 @@
"volume": {
"name": "Volume"
},
"alarm_volume": {
"name": "Alarm volume"
},
"message_volume": {
"name": "Message volume"
},
"guard_return_time": {
"name": "Guard return time"
},
Expand Down
2 changes: 1 addition & 1 deletion tests/components/reolink/snapshots/test_diagnostics.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
}),
'GetDeviceAudioCfg': dict({
'0': 2,
'null': 2,
'null': 4,
}),
'GetEmail': dict({
'0': 1,
Expand Down
44 changes: 44 additions & 0 deletions tests/components/reolink/test_number.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
hass: HomeAssistant,
config_entry: MockConfigEntry,
Expand Down

0 comments on commit 90957df

Please sign in to comment.