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>
This commit is contained in:
Maciej Bieniek 2024-07-15 22:26:12 +02:00 committed by GitHub
parent a9bf12f102
commit 260e98c3f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 424 additions and 32 deletions

View file

@ -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

View file

@ -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