Add support for Shelly `number` virtual component (#121894)
* Support number component in field mode * Support number in label mode * Add tests * Add mode_fn * Add support for number component in slider mode * Add comment * Suggested change * Revert max_fn * Change unit 'min' to 'Hz' in test --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>pull/121973/head^2
parent
a9bf12f102
commit
260e98c3f7
|
@ -61,6 +61,7 @@ PLATFORMS: Final = [
|
|||
Platform.COVER,
|
||||
Platform.EVENT,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TEXT,
|
||||
|
|
|
@ -27,6 +27,8 @@ from aioshelly.const import (
|
|||
MODEL_WALL_DISPLAY,
|
||||
)
|
||||
|
||||
from homeassistant.components.number import NumberMode
|
||||
|
||||
DOMAIN: Final = "shelly"
|
||||
|
||||
LOGGER: Logger = getLogger(__package__)
|
||||
|
@ -240,8 +242,14 @@ CONF_GEN = "gen"
|
|||
SHELLY_PLUS_RGBW_CHANNELS = 4
|
||||
|
||||
VIRTUAL_COMPONENTS_MAP = {
|
||||
"binary_sensor": {"type": "boolean", "mode": "label"},
|
||||
"sensor": {"type": "text", "mode": "label"},
|
||||
"switch": {"type": "boolean", "mode": "toggle"},
|
||||
"text": {"type": "text", "mode": "field"},
|
||||
"binary_sensor": {"types": ["boolean"], "modes": ["label"]},
|
||||
"number": {"types": ["number"], "modes": ["field", "slider"]},
|
||||
"sensor": {"types": ["number", "text"], "modes": ["label"]},
|
||||
"switch": {"types": ["boolean"], "modes": ["toggle"]},
|
||||
"text": {"types": ["text"], "modes": ["field"]},
|
||||
}
|
||||
|
||||
VIRTUAL_NUMBER_MODE_MAP = {
|
||||
"field": NumberMode.BOX,
|
||||
"slider": NumberMode.SLIDER,
|
||||
}
|
||||
|
|
|
@ -291,6 +291,7 @@ class RpcEntityDescription(EntityDescription):
|
|||
extra_state_attributes: Callable[[dict, dict], dict | None] | None = None
|
||||
use_polling_coordinator: bool = False
|
||||
supported: Callable = lambda _: False
|
||||
unit: Callable[[dict], str | None] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
@ -508,6 +509,11 @@ class ShellyRpcAttributeEntity(ShellyRpcEntity, Entity):
|
|||
id_key = key.split(":")[-1]
|
||||
self._id = int(id_key) if id_key.isnumeric() else None
|
||||
|
||||
if callable(description.unit):
|
||||
self._attr_native_unit_of_measurement = description.unit(
|
||||
coordinator.device.config[key]
|
||||
)
|
||||
|
||||
@property
|
||||
def sub_status(self) -> Any:
|
||||
"""Device status by entity key."""
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
from aioshelly.block_device import Block
|
||||
from aioshelly.const import RPC_GENERATIONS
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
|
||||
|
||||
from homeassistant.components.number import (
|
||||
DOMAIN as NUMBER_PLATFORM,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberExtraStoredData,
|
||||
NumberMode,
|
||||
|
@ -20,12 +24,20 @@ from homeassistant.exceptions import HomeAssistantError
|
|||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
from .const import CONF_SLEEP_PERIOD, LOGGER
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry
|
||||
from .const import CONF_SLEEP_PERIOD, LOGGER, VIRTUAL_NUMBER_MODE_MAP
|
||||
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
|
||||
from .entity import (
|
||||
BlockEntityDescription,
|
||||
RpcEntityDescription,
|
||||
ShellyRpcAttributeEntity,
|
||||
ShellySleepingBlockAttributeEntity,
|
||||
async_setup_entry_attribute_entities,
|
||||
async_setup_entry_rpc,
|
||||
)
|
||||
from .utils import (
|
||||
async_remove_orphaned_virtual_entities,
|
||||
get_device_entry_gen,
|
||||
get_virtual_component_ids,
|
||||
)
|
||||
|
||||
|
||||
|
@ -37,6 +49,16 @@ class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription):
|
|||
rest_arg: str = ""
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RpcNumberDescription(RpcEntityDescription, NumberEntityDescription):
|
||||
"""Class to describe a RPC number entity."""
|
||||
|
||||
max_fn: Callable[[dict], float] | None = None
|
||||
min_fn: Callable[[dict], float] | None = None
|
||||
step_fn: Callable[[dict], float] | None = None
|
||||
mode_fn: Callable[[dict], NumberMode] | None = None
|
||||
|
||||
|
||||
NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
|
||||
("device", "valvePos"): BlockNumberDescription(
|
||||
key="device|valvepos",
|
||||
|
@ -55,12 +77,54 @@ NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
|
|||
}
|
||||
|
||||
|
||||
RPC_NUMBERS: Final = {
|
||||
"number": RpcNumberDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
has_entity_name=True,
|
||||
max_fn=lambda config: config["max"],
|
||||
min_fn=lambda config: config["min"],
|
||||
mode_fn=lambda config: VIRTUAL_NUMBER_MODE_MAP.get(
|
||||
config["meta"]["ui"]["view"], NumberMode.BOX
|
||||
),
|
||||
step_fn=lambda config: config["meta"]["ui"]["step"],
|
||||
# If the unit is not set, the device sends an empty string
|
||||
unit=lambda config: config["meta"]["ui"]["unit"]
|
||||
if config["meta"]["ui"]["unit"]
|
||||
else None,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ShellyConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up numbers for device."""
|
||||
if get_device_entry_gen(config_entry) in RPC_GENERATIONS:
|
||||
coordinator = config_entry.runtime_data.rpc
|
||||
assert coordinator
|
||||
|
||||
async_setup_entry_rpc(
|
||||
hass, config_entry, async_add_entities, RPC_NUMBERS, RpcNumber
|
||||
)
|
||||
|
||||
# the user can remove virtual components from the device configuration, so
|
||||
# we need to remove orphaned entities
|
||||
virtual_number_ids = get_virtual_component_ids(
|
||||
coordinator.device.config, NUMBER_PLATFORM
|
||||
)
|
||||
async_remove_orphaned_virtual_entities(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
coordinator.mac,
|
||||
NUMBER_PLATFORM,
|
||||
"number",
|
||||
virtual_number_ids,
|
||||
)
|
||||
return
|
||||
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
async_setup_entry_attribute_entities(
|
||||
hass,
|
||||
|
@ -126,3 +190,44 @@ class BlockSleepingNumber(ShellySleepingBlockAttributeEntity, RestoreNumber):
|
|||
) from err
|
||||
except InvalidAuthError:
|
||||
await self.coordinator.async_shutdown_device_and_start_reauth()
|
||||
|
||||
|
||||
class RpcNumber(ShellyRpcAttributeEntity, NumberEntity):
|
||||
"""Represent a RPC number entity."""
|
||||
|
||||
entity_description: RpcNumberDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: ShellyRpcCoordinator,
|
||||
key: str,
|
||||
attribute: str,
|
||||
description: RpcNumberDescription,
|
||||
) -> None:
|
||||
"""Initialize sensor."""
|
||||
super().__init__(coordinator, key, attribute, description)
|
||||
|
||||
if callable(description.max_fn):
|
||||
self._attr_native_max_value = description.max_fn(
|
||||
coordinator.device.config[key]
|
||||
)
|
||||
if callable(description.min_fn):
|
||||
self._attr_native_min_value = description.min_fn(
|
||||
coordinator.device.config[key]
|
||||
)
|
||||
if callable(description.step_fn):
|
||||
self._attr_native_step = description.step_fn(coordinator.device.config[key])
|
||||
if callable(description.mode_fn):
|
||||
self._attr_mode = description.mode_fn(coordinator.device.config[key])
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return value of number."""
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self.attribute_value, float | None)
|
||||
|
||||
return self.attribute_value
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Change the value."""
|
||||
await self.call_rpc("Number.Set", {"id": self._id, "value": value})
|
||||
|
|
|
@ -1024,6 +1024,14 @@ RPC_SENSORS: Final = {
|
|||
sub_key="value",
|
||||
has_entity_name=True,
|
||||
),
|
||||
"number": RpcSensorDescription(
|
||||
key="number",
|
||||
sub_key="value",
|
||||
has_entity_name=True,
|
||||
unit=lambda config: config["meta"]["ui"]["unit"]
|
||||
if config["meta"]["ui"]["unit"]
|
||||
else None,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -1052,17 +1060,18 @@ async def async_setup_entry(
|
|||
|
||||
# the user can remove virtual components from the device configuration, so
|
||||
# we need to remove orphaned entities
|
||||
virtual_sensor_ids = get_virtual_component_ids(
|
||||
coordinator.device.config, SENSOR_PLATFORM
|
||||
)
|
||||
async_remove_orphaned_virtual_entities(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
coordinator.mac,
|
||||
SENSOR_PLATFORM,
|
||||
"text",
|
||||
virtual_sensor_ids,
|
||||
)
|
||||
for component in ("text", "number"):
|
||||
virtual_component_ids = get_virtual_component_ids(
|
||||
coordinator.device.config, SENSOR_PLATFORM
|
||||
)
|
||||
async_remove_orphaned_virtual_entities(
|
||||
hass,
|
||||
config_entry.entry_id,
|
||||
coordinator.mac,
|
||||
SENSOR_PLATFORM,
|
||||
component,
|
||||
virtual_component_ids,
|
||||
)
|
||||
return
|
||||
|
||||
if config_entry.data[CONF_SLEEP_PERIOD]:
|
||||
|
|
|
@ -323,7 +323,7 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
|
|||
return f"{device_name} {key.replace(':', '_')}"
|
||||
if key.startswith("em1"):
|
||||
return f"{device_name} EM{key.split(':')[-1]}"
|
||||
if key.startswith(("boolean:", "text:")):
|
||||
if key.startswith(("boolean:", "number:", "text:")):
|
||||
return key.replace(":", " ").title()
|
||||
return device_name
|
||||
|
||||
|
@ -524,12 +524,16 @@ def get_virtual_component_ids(config: dict[str, Any], platform: str) -> list[str
|
|||
if not component:
|
||||
return []
|
||||
|
||||
return [
|
||||
k
|
||||
for k, v in config.items()
|
||||
if k.startswith(component["type"])
|
||||
and v["meta"]["ui"]["view"] == component["mode"]
|
||||
]
|
||||
ids: list[str] = []
|
||||
|
||||
for comp_type in component["types"]:
|
||||
ids.extend(
|
||||
k
|
||||
for k, v in config.items()
|
||||
if k.startswith(comp_type) and v["meta"]["ui"]["view"] in component["modes"]
|
||||
)
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
@callback
|
||||
|
|
|
@ -1,18 +1,24 @@
|
|||
"""Tests for Shelly number platform."""
|
||||
|
||||
from copy import deepcopy
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_MAX,
|
||||
ATTR_MIN,
|
||||
ATTR_MODE,
|
||||
ATTR_STEP,
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.components.shelly.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry
|
||||
|
@ -240,3 +246,145 @@ async def test_block_set_value_auth_error(
|
|||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "entity_id", "original_unit", "expected_unit", "view", "mode"),
|
||||
[
|
||||
(
|
||||
"Virtual number",
|
||||
"number.test_name_virtual_number",
|
||||
"%",
|
||||
"%",
|
||||
"field",
|
||||
NumberMode.BOX,
|
||||
),
|
||||
(None, "number.test_name_number_203", "", None, "field", NumberMode.BOX),
|
||||
(
|
||||
"Virtual slider",
|
||||
"number.test_name_virtual_slider",
|
||||
"Hz",
|
||||
"Hz",
|
||||
"slider",
|
||||
NumberMode.SLIDER,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_rpc_device_virtual_number(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
name: str | None,
|
||||
entity_id: str,
|
||||
original_unit: str,
|
||||
expected_unit: str | None,
|
||||
view: str,
|
||||
mode: NumberMode,
|
||||
) -> None:
|
||||
"""Test a virtual number for RPC device."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["number:203"] = {
|
||||
"name": name,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"meta": {"ui": {"step": 0.1, "unit": original_unit, "view": view}},
|
||||
}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
||||
status = deepcopy(mock_rpc_device.status)
|
||||
status["number:203"] = {"value": 12.3}
|
||||
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||
|
||||
await init_integration(hass, 3)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "12.3"
|
||||
assert state.attributes.get(ATTR_MIN) == 0
|
||||
assert state.attributes.get(ATTR_MAX) == 100
|
||||
assert state.attributes.get(ATTR_STEP) == 0.1
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit
|
||||
assert state.attributes.get(ATTR_MODE) is mode
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-number:203-number"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 78.9)
|
||||
mock_rpc_device.mock_update()
|
||||
assert hass.states.get(entity_id).state == "78.9"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 56.7)
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 56.7},
|
||||
blocking=True,
|
||||
)
|
||||
mock_rpc_device.mock_update()
|
||||
assert hass.states.get(entity_id).state == "56.7"
|
||||
|
||||
|
||||
async def test_rpc_remove_virtual_number_when_mode_label(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test if the virtual number will be removed if the mode has been changed to a label."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["number:200"] = {
|
||||
"name": None,
|
||||
"min": -1000,
|
||||
"max": 1000,
|
||||
"meta": {"ui": {"step": 1, "unit": "", "view": "label"}},
|
||||
}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
||||
status = deepcopy(mock_rpc_device.status)
|
||||
status["number:200"] = {"value": 123}
|
||||
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,
|
||||
NUMBER_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
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_number_when_orphaned(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
) -> None:
|
||||
"""Check whether the virtual number 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,
|
||||
NUMBER_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
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
|
||||
|
|
|
@ -863,7 +863,7 @@ async def test_rpc_disabled_xfreq(
|
|||
(None, "sensor.test_name_text_203"),
|
||||
],
|
||||
)
|
||||
async def test_rpc_device_virtual_sensor(
|
||||
async def test_rpc_device_virtual_text_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
|
@ -871,7 +871,7 @@ async def test_rpc_device_virtual_sensor(
|
|||
name: str | None,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Test a virtual sensor for RPC device."""
|
||||
"""Test a virtual text sensor for RPC device."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["text:203"] = {
|
||||
"name": name,
|
||||
|
@ -898,14 +898,14 @@ async def test_rpc_device_virtual_sensor(
|
|||
assert hass.states.get(entity_id).state == "dolor sit amet"
|
||||
|
||||
|
||||
async def test_rpc_remove_virtual_sensor_when_mode_field(
|
||||
async def test_rpc_remove_text_virtual_sensor_when_mode_field(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test if the virtual sensor will be removed if the mode has been changed to a field."""
|
||||
"""Test if the virtual text sensor will be removed if the mode has been changed to a field."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["text:200"] = {"name": None, "meta": {"ui": {"view": "field"}}}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
@ -932,13 +932,13 @@ async def test_rpc_remove_virtual_sensor_when_mode_field(
|
|||
assert not entry
|
||||
|
||||
|
||||
async def test_rpc_remove_virtual_sensor_when_orphaned(
|
||||
async def test_rpc_remove_text_virtual_sensor_when_orphaned(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
) -> None:
|
||||
"""Check whether the virtual sensor will be removed if it has been removed from the device configuration."""
|
||||
"""Check whether the virtual text 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(
|
||||
|
@ -955,3 +955,114 @@ async def test_rpc_remove_virtual_sensor_when_orphaned(
|
|||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert not entry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "entity_id", "original_unit", "expected_unit"),
|
||||
[
|
||||
("Virtual number sensor", "sensor.test_name_virtual_number_sensor", "W", "W"),
|
||||
(None, "sensor.test_name_number_203", "", None),
|
||||
],
|
||||
)
|
||||
async def test_rpc_device_virtual_number_sensor(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
name: str | None,
|
||||
entity_id: str,
|
||||
original_unit: str,
|
||||
expected_unit: str | None,
|
||||
) -> None:
|
||||
"""Test a virtual number sensor for RPC device."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["number:203"] = {
|
||||
"name": name,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"meta": {"ui": {"step": 0.1, "unit": original_unit, "view": "label"}},
|
||||
}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
||||
status = deepcopy(mock_rpc_device.status)
|
||||
status["number:203"] = {"value": 34.5}
|
||||
monkeypatch.setattr(mock_rpc_device, "status", status)
|
||||
|
||||
await init_integration(hass, 3)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "34.5"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == expected_unit
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry
|
||||
assert entry.unique_id == "123456789ABC-number:203-number"
|
||||
|
||||
monkeypatch.setitem(mock_rpc_device.status["number:203"], "value", 56.7)
|
||||
mock_rpc_device.mock_update()
|
||||
assert hass.states.get(entity_id).state == "56.7"
|
||||
|
||||
|
||||
async def test_rpc_remove_number_virtual_sensor_when_mode_field(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test if the virtual number sensor will be removed if the mode has been changed to a field."""
|
||||
config = deepcopy(mock_rpc_device.config)
|
||||
config["number:200"] = {
|
||||
"name": None,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"meta": {"ui": {"step": 1, "unit": "", "view": "field"}},
|
||||
}
|
||||
monkeypatch.setattr(mock_rpc_device, "config", config)
|
||||
|
||||
status = deepcopy(mock_rpc_device.status)
|
||||
status["number:200"] = {"value": 67.8}
|
||||
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,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
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_number_virtual_sensor_when_orphaned(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
mock_rpc_device: Mock,
|
||||
) -> None:
|
||||
"""Check whether the virtual number 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,
|
||||
SENSOR_DOMAIN,
|
||||
"test_name_number_200",
|
||||
"number:200-number",
|
||||
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…
Reference in New Issue