Add Reolink hub status light (#126388)
* Add Home Hub status led * fix styling * Add tests
This commit is contained in:
parent
118ceedda1
commit
bd3efe57f7
3 changed files with 175 additions and 2 deletions
|
@ -20,7 +20,12 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
||||
from .entity import (
|
||||
ReolinkChannelCoordinatorEntity,
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
from .util import ReolinkConfigEntry, ReolinkData
|
||||
|
||||
|
||||
|
@ -37,6 +42,17 @@ class ReolinkLightEntityDescription(
|
|||
turn_on_off_fn: Callable[[Host, int, bool], Any]
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ReolinkHostLightEntityDescription(
|
||||
LightEntityDescription,
|
||||
ReolinkHostEntityDescription,
|
||||
):
|
||||
"""A class that describes host light entities."""
|
||||
|
||||
is_on_fn: Callable[[Host], bool]
|
||||
turn_on_off_fn: Callable[[Host, bool], Any]
|
||||
|
||||
|
||||
LIGHT_ENTITIES = (
|
||||
ReolinkLightEntityDescription(
|
||||
key="floodlight",
|
||||
|
@ -59,6 +75,18 @@ LIGHT_ENTITIES = (
|
|||
),
|
||||
)
|
||||
|
||||
HOST_LIGHT_ENTITIES = (
|
||||
ReolinkHostLightEntityDescription(
|
||||
key="hub_status_led",
|
||||
cmd_key="GetStateLight",
|
||||
translation_key="status_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api: api.supported(None, "state_light"),
|
||||
is_on_fn=lambda api: api.state_light,
|
||||
turn_on_off_fn=lambda api, value: api.set_state_light(value),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
@ -68,13 +96,20 @@ async def async_setup_entry(
|
|||
"""Set up a Reolink light entities."""
|
||||
reolink_data: ReolinkData = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
entities: list[ReolinkLightEntity | ReolinkHostLightEntity] = [
|
||||
ReolinkLightEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in LIGHT_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
if entity_description.supported(reolink_data.host.api, channel)
|
||||
]
|
||||
entities.extend(
|
||||
ReolinkHostLightEntity(reolink_data, entity_description)
|
||||
for entity_description in HOST_LIGHT_ENTITIES
|
||||
if entity_description.supported(reolink_data.host.api)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||
"""Base light entity class for Reolink IP cameras."""
|
||||
|
@ -148,3 +183,41 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
|||
except ReolinkError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostLightEntity(ReolinkHostCoordinatorEntity, LightEntity):
|
||||
"""Base host light entity class for Reolink IP cameras."""
|
||||
|
||||
entity_description: ReolinkHostLightEntityDescription
|
||||
_attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
entity_description: ReolinkHostLightEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink host light entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
return self.entity_description.is_on_fn(self._host.api)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn light off."""
|
||||
try:
|
||||
await self.entity_description.turn_on_off_fn(self._host.api, False)
|
||||
except ReolinkError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn light on."""
|
||||
try:
|
||||
await self.entity_description.turn_on_off_fn(self._host.api, True)
|
||||
except ReolinkError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
self.async_write_ha_state()
|
||||
|
|
|
@ -137,6 +137,9 @@
|
|||
'0': 1,
|
||||
'null': 2,
|
||||
}),
|
||||
'GetStateLight': dict({
|
||||
'null': 1,
|
||||
}),
|
||||
'GetWhiteLed': dict({
|
||||
'0': 3,
|
||||
'null': 3,
|
||||
|
|
|
@ -144,3 +144,100 @@ async def test_light_turn_on(
|
|||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 51},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_host_light_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test host light entity state with status led."""
|
||||
reolink_connect.state_light = True
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.LIGHT]):
|
||||
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.LIGHT}.{TEST_NVR_NAME}_status_led"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_host_light_turn_off(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test host light turn off service."""
|
||||
|
||||
def mock_supported(ch, capability):
|
||||
if capability == "power_led":
|
||||
return False
|
||||
return True
|
||||
|
||||
reolink_connect.supported = mock_supported
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.LIGHT]):
|
||||
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.LIGHT}.{TEST_NVR_NAME}_status_led"
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
reolink_connect.set_state_light.assert_called_with(False)
|
||||
|
||||
reolink_connect.set_state_light.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_host_light_turn_on(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
reolink_connect: MagicMock,
|
||||
) -> None:
|
||||
"""Test host light turn on service."""
|
||||
|
||||
def mock_supported(ch, capability):
|
||||
if capability == "power_led":
|
||||
return False
|
||||
return True
|
||||
|
||||
reolink_connect.supported = mock_supported
|
||||
|
||||
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.LIGHT]):
|
||||
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.LIGHT}.{TEST_NVR_NAME}_status_led"
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
reolink_connect.set_state_light.assert_called_with(True)
|
||||
|
||||
reolink_connect.set_state_light.side_effect = ReolinkError("Test error")
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
|
Loading…
Add table
Reference in a new issue