mirror of
https://github.com/home-assistant/core.git
synced 2025-04-26 10:17:51 +00:00
Add Reolink hub status light (#126388)
* Add Home Hub status led * fix styling * Add tests
This commit is contained in:
parent
118ceedda1
commit
bd3efe57f7
@ -20,7 +20,12 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .entity import ReolinkChannelCoordinatorEntity, ReolinkChannelEntityDescription
|
from .entity import (
|
||||||
|
ReolinkChannelCoordinatorEntity,
|
||||||
|
ReolinkChannelEntityDescription,
|
||||||
|
ReolinkHostCoordinatorEntity,
|
||||||
|
ReolinkHostEntityDescription,
|
||||||
|
)
|
||||||
from .util import ReolinkConfigEntry, ReolinkData
|
from .util import ReolinkConfigEntry, ReolinkData
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +42,17 @@ class ReolinkLightEntityDescription(
|
|||||||
turn_on_off_fn: Callable[[Host, int, bool], Any]
|
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 = (
|
LIGHT_ENTITIES = (
|
||||||
ReolinkLightEntityDescription(
|
ReolinkLightEntityDescription(
|
||||||
key="floodlight",
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -68,13 +96,20 @@ async def async_setup_entry(
|
|||||||
"""Set up a Reolink light entities."""
|
"""Set up a Reolink light entities."""
|
||||||
reolink_data: ReolinkData = config_entry.runtime_data
|
reolink_data: ReolinkData = config_entry.runtime_data
|
||||||
|
|
||||||
async_add_entities(
|
entities: list[ReolinkLightEntity | ReolinkHostLightEntity] = [
|
||||||
ReolinkLightEntity(reolink_data, channel, entity_description)
|
ReolinkLightEntity(reolink_data, channel, entity_description)
|
||||||
for entity_description in LIGHT_ENTITIES
|
for entity_description in LIGHT_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(
|
||||||
|
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):
|
class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
||||||
"""Base light entity class for Reolink IP cameras."""
|
"""Base light entity class for Reolink IP cameras."""
|
||||||
@ -148,3 +183,41 @@ class ReolinkLightEntity(ReolinkChannelCoordinatorEntity, LightEntity):
|
|||||||
except ReolinkError as err:
|
except ReolinkError as err:
|
||||||
raise HomeAssistantError(err) from err
|
raise HomeAssistantError(err) from err
|
||||||
self.async_write_ha_state()
|
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,
|
'0': 1,
|
||||||
'null': 2,
|
'null': 2,
|
||||||
}),
|
}),
|
||||||
|
'GetStateLight': dict({
|
||||||
|
'null': 1,
|
||||||
|
}),
|
||||||
'GetWhiteLed': dict({
|
'GetWhiteLed': dict({
|
||||||
'0': 3,
|
'0': 3,
|
||||||
'null': 3,
|
'null': 3,
|
||||||
|
@ -144,3 +144,100 @@ async def test_light_turn_on(
|
|||||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 51},
|
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 51},
|
||||||
blocking=True,
|
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…
x
Reference in New Issue
Block a user