mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Add support for Shelly virtual boolean
component (#119932)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
This commit is contained in:
parent
311b1e236a
commit
70f05e5f13
@ -8,6 +8,7 @@ from typing import Final, cast
|
|||||||
from aioshelly.const import RPC_GENERATIONS
|
from aioshelly.const import RPC_GENERATIONS
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
DOMAIN as BINARY_SENSOR_PLATFORM,
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
@ -33,7 +34,9 @@ from .entity import (
|
|||||||
async_setup_entry_rpc,
|
async_setup_entry_rpc,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
async_remove_orphaned_virtual_entities,
|
||||||
get_device_entry_gen,
|
get_device_entry_gen,
|
||||||
|
get_virtual_component_ids,
|
||||||
is_block_momentary_input,
|
is_block_momentary_input,
|
||||||
is_rpc_momentary_input,
|
is_rpc_momentary_input,
|
||||||
)
|
)
|
||||||
@ -215,6 +218,11 @@ RPC_SENSORS: Final = {
|
|||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
|
"boolean": RpcBinarySensorDescription(
|
||||||
|
key="boolean",
|
||||||
|
sub_key="value",
|
||||||
|
has_entity_name=True,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -234,9 +242,26 @@ async def async_setup_entry(
|
|||||||
RpcSleepingBinarySensor,
|
RpcSleepingBinarySensor,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
coordinator = config_entry.runtime_data.rpc
|
||||||
|
assert coordinator
|
||||||
|
|
||||||
async_setup_entry_rpc(
|
async_setup_entry_rpc(
|
||||||
hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor
|
hass, config_entry, async_add_entities, RPC_SENSORS, RpcBinarySensor
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# the user can remove virtual components from the device configuration, so
|
||||||
|
# we need to remove orphaned entities
|
||||||
|
virtual_binary_sensor_ids = get_virtual_component_ids(
|
||||||
|
coordinator.device.config, BINARY_SENSOR_PLATFORM
|
||||||
|
)
|
||||||
|
async_remove_orphaned_virtual_entities(
|
||||||
|
hass,
|
||||||
|
config_entry.entry_id,
|
||||||
|
coordinator.mac,
|
||||||
|
BINARY_SENSOR_PLATFORM,
|
||||||
|
"boolean",
|
||||||
|
virtual_binary_sensor_ids,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||||
|
@ -238,3 +238,8 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = (
|
|||||||
CONF_GEN = "gen"
|
CONF_GEN = "gen"
|
||||||
|
|
||||||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||||
|
|
||||||
|
VIRTUAL_COMPONENTS_MAP = {
|
||||||
|
"binary_sensor": {"type": "boolean", "mode": "label"},
|
||||||
|
"switch": {"type": "boolean", "mode": "toggle"},
|
||||||
|
}
|
||||||
|
@ -551,7 +551,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
|||||||
for event_callback in self._event_listeners:
|
for event_callback in self._event_listeners:
|
||||||
event_callback(event)
|
event_callback(event)
|
||||||
|
|
||||||
if event_type == "config_changed":
|
if event_type in ("component_added", "component_removed", "config_changed"):
|
||||||
self.update_sleep_period()
|
self.update_sleep_period()
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Config for %s changed, reloading entry in %s seconds",
|
"Config for %s changed, reloading entry in %s seconds",
|
||||||
@ -739,6 +739,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
|||||||
LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
|
LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
|
||||||
try:
|
try:
|
||||||
await self.device.update_status()
|
await self.device.update_status()
|
||||||
|
await self.device.get_dynamic_components()
|
||||||
except (DeviceConnectionError, RpcCallError) as err:
|
except (DeviceConnectionError, RpcCallError) as err:
|
||||||
raise UpdateFailed(f"Device disconnected: {err!r}") from err
|
raise UpdateFailed(f"Device disconnected: {err!r}") from err
|
||||||
except InvalidAuthError:
|
except InvalidAuthError:
|
||||||
|
@ -505,6 +505,8 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, Entity):
|
|||||||
self._attr_unique_id = f"{super().unique_id}-{attribute}"
|
self._attr_unique_id = f"{super().unique_id}-{attribute}"
|
||||||
self._attr_name = get_rpc_entity_name(coordinator.device, key, description.name)
|
self._attr_name = get_rpc_entity_name(coordinator.device, key, description.name)
|
||||||
self._last_value = None
|
self._last_value = None
|
||||||
|
id_key = key.split(":")[-1]
|
||||||
|
self._id = int(id_key) if id_key.isnumeric() else None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sub_status(self) -> Any:
|
def sub_status(self) -> Any:
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioshelly"],
|
"loggers": ["aioshelly"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioshelly==11.0.0"],
|
"requirements": ["aioshelly==11.1.0"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_http._tcp.local.",
|
"type": "_http._tcp.local.",
|
||||||
|
@ -8,7 +8,11 @@ from typing import Any, cast
|
|||||||
from aioshelly.block_device import Block
|
from aioshelly.block_device import Block
|
||||||
from aioshelly.const import MODEL_2, MODEL_25, MODEL_WALL_DISPLAY, RPC_GENERATIONS
|
from aioshelly.const import MODEL_2, MODEL_25, MODEL_WALL_DISPLAY, RPC_GENERATIONS
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import (
|
||||||
|
DOMAIN as SWITCH_PLATFORM,
|
||||||
|
SwitchEntity,
|
||||||
|
SwitchEntityDescription,
|
||||||
|
)
|
||||||
from homeassistant.const import STATE_ON, EntityCategory
|
from homeassistant.const import STATE_ON, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, State, callback
|
from homeassistant.core import HomeAssistant, State, callback
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
@ -19,15 +23,20 @@ from .const import CONF_SLEEP_PERIOD, MOTION_MODELS
|
|||||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||||
from .entity import (
|
from .entity import (
|
||||||
BlockEntityDescription,
|
BlockEntityDescription,
|
||||||
|
RpcEntityDescription,
|
||||||
ShellyBlockEntity,
|
ShellyBlockEntity,
|
||||||
|
ShellyRpcAttributeEntity,
|
||||||
ShellyRpcEntity,
|
ShellyRpcEntity,
|
||||||
ShellySleepingBlockAttributeEntity,
|
ShellySleepingBlockAttributeEntity,
|
||||||
async_setup_entry_attribute_entities,
|
async_setup_entry_attribute_entities,
|
||||||
|
async_setup_rpc_attribute_entities,
|
||||||
)
|
)
|
||||||
from .utils import (
|
from .utils import (
|
||||||
|
async_remove_orphaned_virtual_entities,
|
||||||
async_remove_shelly_entity,
|
async_remove_shelly_entity,
|
||||||
get_device_entry_gen,
|
get_device_entry_gen,
|
||||||
get_rpc_key_ids,
|
get_rpc_key_ids,
|
||||||
|
get_virtual_component_ids,
|
||||||
is_block_channel_type_light,
|
is_block_channel_type_light,
|
||||||
is_rpc_channel_type_light,
|
is_rpc_channel_type_light,
|
||||||
is_rpc_thermostat_internal_actuator,
|
is_rpc_thermostat_internal_actuator,
|
||||||
@ -47,6 +56,17 @@ MOTION_SWITCH = BlockSwitchDescription(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class RpcSwitchDescription(RpcEntityDescription, SwitchEntityDescription):
|
||||||
|
"""Class to describe a RPC virtual switch."""
|
||||||
|
|
||||||
|
|
||||||
|
RPC_VIRTUAL_SWITCH = RpcSwitchDescription(
|
||||||
|
key="boolean",
|
||||||
|
sub_key="value",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ShellyConfigEntry,
|
config_entry: ShellyConfigEntry,
|
||||||
@ -148,6 +168,28 @@ def async_setup_rpc_entry(
|
|||||||
unique_id = f"{coordinator.mac}-switch:{id_}"
|
unique_id = f"{coordinator.mac}-switch:{id_}"
|
||||||
async_remove_shelly_entity(hass, "light", unique_id)
|
async_remove_shelly_entity(hass, "light", unique_id)
|
||||||
|
|
||||||
|
async_setup_rpc_attribute_entities(
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
async_add_entities,
|
||||||
|
{"boolean": RPC_VIRTUAL_SWITCH},
|
||||||
|
RpcVirtualSwitch,
|
||||||
|
)
|
||||||
|
|
||||||
|
# the user can remove virtual components from the device configuration, so we need
|
||||||
|
# to remove orphaned entities
|
||||||
|
virtual_switch_ids = get_virtual_component_ids(
|
||||||
|
coordinator.device.config, SWITCH_PLATFORM
|
||||||
|
)
|
||||||
|
async_remove_orphaned_virtual_entities(
|
||||||
|
hass,
|
||||||
|
config_entry.entry_id,
|
||||||
|
coordinator.mac,
|
||||||
|
SWITCH_PLATFORM,
|
||||||
|
"boolean",
|
||||||
|
virtual_switch_ids,
|
||||||
|
)
|
||||||
|
|
||||||
if not switch_ids:
|
if not switch_ids:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -255,3 +297,23 @@ class RpcRelaySwitch(ShellyRpcEntity, SwitchEntity):
|
|||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
"""Turn off relay."""
|
"""Turn off relay."""
|
||||||
await self.call_rpc("Switch.Set", {"id": self._id, "on": False})
|
await self.call_rpc("Switch.Set", {"id": self._id, "on": False})
|
||||||
|
|
||||||
|
|
||||||
|
class RpcVirtualSwitch(ShellyRpcAttributeEntity, SwitchEntity):
|
||||||
|
"""Entity that controls a virtual boolean component on RPC based Shelly devices."""
|
||||||
|
|
||||||
|
entity_description: RpcSwitchDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""If switch is on."""
|
||||||
|
return bool(self.attribute_value)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on relay."""
|
||||||
|
await self.call_rpc("Boolean.Set", {"id": self._id, "value": True})
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off relay."""
|
||||||
|
await self.call_rpc("Boolean.Set", {"id": self._id, "value": False})
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from ipaddress import IPv4Address
|
from ipaddress import IPv4Address
|
||||||
|
import re
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@ -52,6 +53,7 @@ from .const import (
|
|||||||
SHBTN_MODELS,
|
SHBTN_MODELS,
|
||||||
SHIX3_1_INPUTS_EVENTS_TYPES,
|
SHIX3_1_INPUTS_EVENTS_TYPES,
|
||||||
UPTIME_DEVIATION,
|
UPTIME_DEVIATION,
|
||||||
|
VIRTUAL_COMPONENTS_MAP,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -321,6 +323,8 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
|
|||||||
return f"{device_name} {key.replace(':', '_')}"
|
return f"{device_name} {key.replace(':', '_')}"
|
||||||
if key.startswith("em1"):
|
if key.startswith("em1"):
|
||||||
return f"{device_name} EM{key.split(':')[-1]}"
|
return f"{device_name} EM{key.split(':')[-1]}"
|
||||||
|
if key.startswith("boolean:"):
|
||||||
|
return key.replace(":", " ").title()
|
||||||
return device_name
|
return device_name
|
||||||
|
|
||||||
return entity_name
|
return entity_name
|
||||||
@ -497,3 +501,55 @@ def async_remove_shelly_rpc_entities(
|
|||||||
def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool:
|
def is_rpc_thermostat_mode(ident: int, status: dict[str, Any]) -> bool:
|
||||||
"""Return True if 'thermostat:<IDent>' is present in the status."""
|
"""Return True if 'thermostat:<IDent>' is present in the status."""
|
||||||
return f"thermostat:{ident}" in status
|
return f"thermostat:{ident}" in status
|
||||||
|
|
||||||
|
|
||||||
|
def get_virtual_component_ids(config: dict[str, Any], platform: str) -> list[str]:
|
||||||
|
"""Return a list of virtual component IDs for a platform."""
|
||||||
|
component = VIRTUAL_COMPONENTS_MAP.get(platform)
|
||||||
|
|
||||||
|
if not component:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
k
|
||||||
|
for k, v in config.items()
|
||||||
|
if k.startswith(component["type"])
|
||||||
|
and v["meta"]["ui"]["view"] == component["mode"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_remove_orphaned_virtual_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry_id: str,
|
||||||
|
mac: str,
|
||||||
|
platform: str,
|
||||||
|
virt_comp_type: str,
|
||||||
|
virt_comp_ids: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Remove orphaned virtual entities."""
|
||||||
|
orphaned_entities = []
|
||||||
|
entity_reg = er.async_get(hass)
|
||||||
|
device_reg = dr.async_get(hass)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id)
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
device_id = devices[0].id
|
||||||
|
entities = er.async_entries_for_device(entity_reg, device_id, True)
|
||||||
|
for entity in entities:
|
||||||
|
if not entity.entity_id.startswith(platform):
|
||||||
|
continue
|
||||||
|
if virt_comp_type not in entity.unique_id:
|
||||||
|
continue
|
||||||
|
# we are looking for the component ID, e.g. boolean:201
|
||||||
|
if not (match := re.search(r"[a-z]+:\d+", entity.unique_id)):
|
||||||
|
continue
|
||||||
|
virt_comp_id = match.group()
|
||||||
|
if virt_comp_id not in virt_comp_ids:
|
||||||
|
orphaned_entities.append(f"{virt_comp_id}-{virt_comp_type}")
|
||||||
|
|
||||||
|
if orphaned_entities:
|
||||||
|
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
|
||||||
|
@ -365,7 +365,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==11.0.0
|
aioshelly==11.1.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
|
@ -338,7 +338,7 @@ aioruuvigateway==0.1.0
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==11.0.0
|
aioshelly==11.1.0
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
|
@ -23,6 +23,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.device_registry import (
|
from homeassistant.helpers.device_registry import (
|
||||||
CONNECTION_NETWORK_MAC,
|
CONNECTION_NETWORK_MAC,
|
||||||
|
DeviceEntry,
|
||||||
DeviceRegistry,
|
DeviceRegistry,
|
||||||
format_mac,
|
format_mac,
|
||||||
)
|
)
|
||||||
@ -111,6 +112,7 @@ def register_entity(
|
|||||||
unique_id: str,
|
unique_id: str,
|
||||||
config_entry: ConfigEntry | None = None,
|
config_entry: ConfigEntry | None = None,
|
||||||
capabilities: Mapping[str, Any] | None = None,
|
capabilities: Mapping[str, Any] | None = None,
|
||||||
|
device_id: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Register enabled entity, return entity_id."""
|
"""Register enabled entity, return entity_id."""
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
@ -122,6 +124,7 @@ def register_entity(
|
|||||||
disabled_by=None,
|
disabled_by=None,
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
capabilities=capabilities,
|
capabilities=capabilities,
|
||||||
|
device_id=device_id,
|
||||||
)
|
)
|
||||||
return f"{domain}.{object_id}"
|
return f"{domain}.{object_id}"
|
||||||
|
|
||||||
@ -145,9 +148,11 @@ def get_entity_state(hass: HomeAssistant, entity_id: str) -> str:
|
|||||||
return entity.state
|
return entity.state
|
||||||
|
|
||||||
|
|
||||||
def register_device(device_registry: DeviceRegistry, config_entry: ConfigEntry) -> None:
|
def register_device(
|
||||||
|
device_registry: DeviceRegistry, config_entry: ConfigEntry
|
||||||
|
) -> DeviceEntry:
|
||||||
"""Register Shelly device."""
|
"""Register Shelly device."""
|
||||||
device_registry.async_get_or_create(
|
return device_registry.async_get_or_create(
|
||||||
config_entry_id=config_entry.entry_id,
|
config_entry_id=config_entry.entry_id,
|
||||||
connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))},
|
connections={(CONNECTION_NETWORK_MAC, format_mac(MOCK_MAC))},
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Tests for Shelly binary sensor platform."""
|
"""Tests for Shelly binary sensor platform."""
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from aioshelly.const import MODEL_MOTION
|
from aioshelly.const import MODEL_MOTION
|
||||||
@ -10,6 +11,7 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI
|
|||||||
from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER
|
from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
@ -353,3 +355,125 @@ async def test_rpc_restored_sleeping_binary_sensor_no_last_state(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert hass.states.get(entity_id).state == STATE_OFF
|
assert hass.states.get(entity_id).state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_device_virtual_binary_sensor_with_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test a virtual binary sensor for RPC device."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:203"] = {
|
||||||
|
"name": "Virtual binary sensor",
|
||||||
|
"meta": {"ui": {"view": "label"}},
|
||||||
|
}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:203"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
entity_id = "binary_sensor.test_name_virtual_binary_sensor"
|
||||||
|
|
||||||
|
await init_integration(hass, 3)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "123456789ABC-boolean:203-boolean"
|
||||||
|
|
||||||
|
monkeypatch.setitem(mock_rpc_device.status["boolean:203"], "value", False)
|
||||||
|
mock_rpc_device.mock_update()
|
||||||
|
assert hass.states.get(entity_id).state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_device_virtual_binary_sensor_without_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test a virtual binary sensor for RPC device."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:203"] = {"name": None, "meta": {"ui": {"view": "label"}}}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:203"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
entity_id = "binary_sensor.test_name_boolean_203"
|
||||||
|
|
||||||
|
await init_integration(hass, 3)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "123456789ABC-boolean:203-boolean"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_remove_virtual_binary_sensor_when_mode_toggle(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test if the virtual binary sensor will be removed if the mode has been changed to a toggle."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:200"] = {"name": None, "meta": {"ui": {"view": "toggle"}}}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:200"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
config_entry = await init_integration(hass, 3, skip_setup=True)
|
||||||
|
device_entry = register_device(device_registry, config_entry)
|
||||||
|
entity_id = register_entity(
|
||||||
|
hass,
|
||||||
|
BINARY_SENSOR_DOMAIN,
|
||||||
|
"test_name_boolean_200",
|
||||||
|
"boolean:200-boolean",
|
||||||
|
config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert not entry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Check whether the virtual binary sensor will be removed if it has been removed from the device configuration."""
|
||||||
|
config_entry = await init_integration(hass, 3, skip_setup=True)
|
||||||
|
device_entry = register_device(device_registry, config_entry)
|
||||||
|
entity_id = register_entity(
|
||||||
|
hass,
|
||||||
|
BINARY_SENSOR_DOMAIN,
|
||||||
|
"test_name_boolean_200",
|
||||||
|
"boolean:200-boolean",
|
||||||
|
config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert not entry
|
||||||
|
@ -25,6 +25,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
@ -430,3 +431,163 @@ async def test_wall_display_relay_mode(
|
|||||||
entry = entity_registry.async_get(switch_entity_id)
|
entry = entity_registry.async_get(switch_entity_id)
|
||||||
assert entry
|
assert entry
|
||||||
assert entry.unique_id == "123456789ABC-switch:0"
|
assert entry.unique_id == "123456789ABC-switch:0"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_device_virtual_switch_with_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test a virtual switch for RPC device."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:200"] = {
|
||||||
|
"name": "Virtual switch",
|
||||||
|
"meta": {"ui": {"view": "toggle"}},
|
||||||
|
}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:200"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
entity_id = "switch.test_name_virtual_switch"
|
||||||
|
|
||||||
|
await init_integration(hass, 3)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "123456789ABC-boolean:200-boolean"
|
||||||
|
|
||||||
|
monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", False)
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_rpc_device.mock_update()
|
||||||
|
assert hass.states.get(entity_id).state == STATE_OFF
|
||||||
|
|
||||||
|
monkeypatch.setitem(mock_rpc_device.status["boolean:200"], "value", True)
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_rpc_device.mock_update()
|
||||||
|
assert hass.states.get(entity_id).state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_device_virtual_switch_without_name(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test a virtual switch for RPC device."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:200"] = {"name": None, "meta": {"ui": {"view": "toggle"}}}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:200"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
entity_id = "switch.test_name_boolean_200"
|
||||||
|
|
||||||
|
await init_integration(hass, 3)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert entry
|
||||||
|
assert entry.unique_id == "123456789ABC-boolean:200-boolean"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_device_virtual_binary_sensor(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test that a switch entity has not been created for a virtual binary sensor."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:200"] = {"name": None, "meta": {"ui": {"view": "label"}}}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:200"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
entity_id = "switch.test_name_boolean_200"
|
||||||
|
|
||||||
|
await init_integration(hass, 3)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert not state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_remove_virtual_switch_when_mode_label(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test if the virtual switch will be removed if the mode has been changed to a label."""
|
||||||
|
config = deepcopy(mock_rpc_device.config)
|
||||||
|
config["boolean:200"] = {"name": None, "meta": {"ui": {"view": "label"}}}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||||
|
|
||||||
|
status = deepcopy(mock_rpc_device.status)
|
||||||
|
status["boolean:200"] = {"value": True}
|
||||||
|
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||||
|
|
||||||
|
config_entry = await init_integration(hass, 3, skip_setup=True)
|
||||||
|
device_entry = register_device(device_registry, config_entry)
|
||||||
|
entity_id = register_entity(
|
||||||
|
hass,
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
"test_name_boolean_200",
|
||||||
|
"boolean:200-boolean",
|
||||||
|
config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert not entry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_rpc_remove_virtual_switch_when_orphaned(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
device_registry: DeviceRegistry,
|
||||||
|
mock_rpc_device: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Check whether the virtual switch will be removed if it has been removed from the device configuration."""
|
||||||
|
config_entry = await init_integration(hass, 3, skip_setup=True)
|
||||||
|
device_entry = register_device(device_registry, config_entry)
|
||||||
|
entity_id = register_entity(
|
||||||
|
hass,
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
"test_name_boolean_200",
|
||||||
|
"boolean:200-boolean",
|
||||||
|
config_entry,
|
||||||
|
device_id=device_entry.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entry = entity_registry.async_get(entity_id)
|
||||||
|
assert not entry
|
||||||
|
Loading…
x
Reference in New Issue
Block a user