Add additional entities for Shelly BLU TRV (#135244)

* Add valve position sensor

* Add valve position and external sensor temperature numbers

* Fix method name

* Better name

* Add remove condition

* Add calibration binary sensor

* Add battery and signal strength sensors

* Remove condition from ShellyRpcEntity

* Typo

* Add get_entity_class helper

* Add tests

* Use snapshots in tests
This commit is contained in:
Maciej Bieniek 2025-01-20 22:11:20 +00:00 committed by GitHub
parent d7ec99de7d
commit 11d44e608b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 647 additions and 90 deletions

View File

@ -15,11 +15,12 @@ from homeassistant.components.binary_sensor import (
)
from homeassistant.const import STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import CONF_SLEEP_PERIOD
from .coordinator import ShellyConfigEntry
from .coordinator import ShellyConfigEntry, ShellyRpcCoordinator
from .entity import (
BlockEntityDescription,
RestEntityDescription,
@ -59,6 +60,36 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr
"""Class to describe a REST binary sensor."""
class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity):
"""Represent a RPC binary sensor entity."""
entity_description: RpcBinarySensorDescription
@property
def is_on(self) -> bool:
"""Return true if RPC sensor state is on."""
return bool(self.attribute_value)
class RpcBluTrvBinarySensor(RpcBinarySensor):
"""Represent a RPC BluTrv binary sensor."""
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcBinarySensorDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, ble_addr)}
)
SENSORS: dict[tuple[str, str], BlockBinarySensorDescription] = {
("device", "overtemp"): BlockBinarySensorDescription(
key="device|overtemp",
@ -232,6 +263,15 @@ RPC_SENSORS: Final = {
sub_key="value",
has_entity_name=True,
),
"calibration": RpcBinarySensorDescription(
key="blutrv",
sub_key="errors",
name="Calibration",
device_class=BinarySensorDeviceClass.PROBLEM,
value=lambda status, _: False if status is None else "not_calibrated" in status,
entity_category=EntityCategory.DIAGNOSTIC,
entity_class=RpcBluTrvBinarySensor,
),
}
@ -320,17 +360,6 @@ class RestBinarySensor(ShellyRestAttributeEntity, BinarySensorEntity):
return bool(self.attribute_value)
class RpcBinarySensor(ShellyRpcAttributeEntity, BinarySensorEntity):
"""Represent a RPC binary sensor entity."""
entity_description: RpcBinarySensorDescription
@property
def is_on(self) -> bool:
"""Return true if RPC sensor state is on."""
return bool(self.attribute_value)
class BlockSleepingBinarySensor(
ShellySleepingBlockAttributeEntity, BinarySensorEntity, RestoreEntity
):

View File

@ -196,10 +196,16 @@ def async_setup_rpc_attribute_entities(
elif description.use_polling_coordinator:
if not sleep_period:
entities.append(
sensor_class(polling_coordinator, key, sensor_id, description)
get_entity_class(sensor_class, description)(
polling_coordinator, key, sensor_id, description
)
)
else:
entities.append(sensor_class(coordinator, key, sensor_id, description))
entities.append(
get_entity_class(sensor_class, description)(
coordinator, key, sensor_id, description
)
)
if not entities:
return
@ -232,7 +238,9 @@ def async_restore_rpc_attribute_entities(
if description := sensors.get(attribute):
entities.append(
sensor_class(coordinator, key, attribute, description, entry)
get_entity_class(sensor_class, description)(
coordinator, key, attribute, description, entry
)
)
if not entities:
@ -293,6 +301,7 @@ class RpcEntityDescription(EntityDescription):
supported: Callable = lambda _: False
unit: Callable[[dict], str | None] | None = None
options_fn: Callable[[dict], list[str]] | None = None
entity_class: Callable | None = None
@dataclass(frozen=True)
@ -673,3 +682,13 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity):
"Entity %s comes from a sleeping device, update is not possible",
self.entity_id,
)
def get_entity_class(
sensor_class: Callable, description: RpcEntityDescription
) -> Callable:
"""Return entity class."""
if description.entity_class is not None:
return description.entity_class
return sensor_class

View File

@ -12,6 +12,9 @@
}
},
"number": {
"external_temperature": {
"default": "mdi:thermometer-check"
},
"valve_position": {
"default": "mdi:pipe-valve"
}
@ -29,6 +32,9 @@
"tilt": {
"default": "mdi:angle-acute"
},
"valve_position": {
"default": "mdi:pipe-valve"
},
"valve_status": {
"default": "mdi:valve"
}

View File

@ -18,9 +18,10 @@ from homeassistant.components.number import (
NumberMode,
RestoreNumber,
)
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
@ -57,6 +58,74 @@ class RpcNumberDescription(RpcEntityDescription, NumberEntityDescription):
min_fn: Callable[[dict], float] | None = None
step_fn: Callable[[dict], float] | None = None
mode_fn: Callable[[dict], NumberMode] | None = None
method: str
method_params_fn: Callable[[int, float], dict]
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 description.max_fn is not None:
self._attr_native_max_value = description.max_fn(
coordinator.device.config[key]
)
if description.min_fn is not None:
self._attr_native_min_value = description.min_fn(
coordinator.device.config[key]
)
if description.step_fn is not None:
self._attr_native_step = description.step_fn(coordinator.device.config[key])
if description.mode_fn is not None:
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."""
if TYPE_CHECKING:
assert isinstance(self._id, int)
await self.call_rpc(
self.entity_description.method,
self.entity_description.method_params_fn(self._id, value),
)
class RpcBluTrvNumber(RpcNumber):
"""Represent a RPC BluTrv number."""
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcNumberDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, ble_addr)}
)
NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
@ -78,6 +147,25 @@ NUMBERS: dict[tuple[str, str], BlockNumberDescription] = {
RPC_NUMBERS: Final = {
"external_temperature": RpcNumberDescription(
key="blutrv",
sub_key="current_C",
translation_key="external_temperature",
name="External temperature",
native_min_value=-50,
native_max_value=50,
native_step=0.1,
mode=NumberMode.BOX,
entity_category=EntityCategory.CONFIG,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
method="BluTRV.Call",
method_params_fn=lambda idx, value: {
"id": idx,
"method": "Trv.SetExternalTemperature",
"params": {"id": 0, "t_C": value},
},
entity_class=RpcBluTrvNumber,
),
"number": RpcNumberDescription(
key="number",
sub_key="value",
@ -92,6 +180,28 @@ RPC_NUMBERS: Final = {
unit=lambda config: config["meta"]["ui"]["unit"]
if config["meta"]["ui"]["unit"]
else None,
method="Number.Set",
method_params_fn=lambda idx, value: {"id": idx, "value": value},
),
"valve_position": RpcNumberDescription(
key="blutrv",
sub_key="pos",
translation_key="valve_position",
name="Valve position",
native_min_value=0,
native_max_value=100,
native_step=1,
mode=NumberMode.SLIDER,
native_unit_of_measurement=PERCENTAGE,
method="BluTRV.Call",
method_params_fn=lambda idx, value: {
"id": idx,
"method": "Trv.SetPosition",
"params": {"id": 0, "pos": value},
},
removal_condition=lambda config, _status, key: config[key].get("enable", True)
is True,
entity_class=RpcBluTrvNumber,
),
}
@ -190,44 +300,3 @@ 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 description.max_fn is not None:
self._attr_native_max_value = description.max_fn(
coordinator.device.config[key]
)
if description.min_fn is not None:
self._attr_native_min_value = description.min_fn(
coordinator.device.config[key]
)
if description.step_fn is not None:
self._attr_native_step = description.step_fn(coordinator.device.config[key])
if description.mode_fn is not None:
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})

View File

@ -33,6 +33,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_registry import RegistryEntry
from homeassistant.helpers.typing import StateType
@ -76,6 +77,57 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription):
"""Class to describe a REST sensor."""
class RpcSensor(ShellyRpcAttributeEntity, SensorEntity):
"""Represent a RPC sensor."""
entity_description: RpcSensorDescription
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcSensorDescription,
) -> None:
"""Initialize select."""
super().__init__(coordinator, key, attribute, description)
if self.option_map:
self._attr_options = list(self.option_map.values())
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
attribute_value = self.attribute_value
if not self.option_map:
return attribute_value
if not isinstance(attribute_value, str):
return None
return self.option_map[attribute_value]
class RpcBluTrvSensor(RpcSensor):
"""Represent a RPC BluTrv sensor."""
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcSensorDescription,
) -> None:
"""Initialize."""
super().__init__(coordinator, key, attribute, description)
ble_addr: str = coordinator.device.config[key]["addr"]
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_BLUETOOTH, ble_addr)}
)
SENSORS: dict[tuple[str, str], BlockSensorDescription] = {
("device", "battery"): BlockSensorDescription(
key="device|battery",
@ -1222,6 +1274,38 @@ RPC_SENSORS: Final = {
options_fn=lambda config: config["options"],
device_class=SensorDeviceClass.ENUM,
),
"valve_position": RpcSensorDescription(
key="blutrv",
sub_key="pos",
name="Valve position",
translation_key="valve_position",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
removal_condition=lambda config, _status, key: config[key].get("enable", False)
is False,
entity_class=RpcBluTrvSensor,
),
"blutrv_battery": RpcSensorDescription(
key="blutrv",
sub_key="battery",
name="Battery",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_class=RpcBluTrvSensor,
),
"blutrv_rssi": RpcSensorDescription(
key="blutrv",
sub_key="rssi",
name="Signal strength",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_class=RpcBluTrvSensor,
),
}
@ -1327,38 +1411,6 @@ class RestSensor(ShellyRestAttributeEntity, SensorEntity):
return self.attribute_value
class RpcSensor(ShellyRpcAttributeEntity, SensorEntity):
"""Represent a RPC sensor."""
entity_description: RpcSensorDescription
def __init__(
self,
coordinator: ShellyRpcCoordinator,
key: str,
attribute: str,
description: RpcSensorDescription,
) -> None:
"""Initialize select."""
super().__init__(coordinator, key, attribute, description)
if self.option_map:
self._attr_options = list(self.option_map.values())
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
attribute_value = self.attribute_value
if not self.option_map:
return attribute_value
if not isinstance(attribute_value, str):
return None
return self.option_map[attribute_value]
class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, RestoreSensor):
"""Represent a block sleeping sensor."""

View File

@ -255,6 +255,8 @@ MOCK_BLU_TRV_REMOTE_STATUS = {
"current_C": 15.2,
"target_C": 17.1,
"schedule_rev": 0,
"rssi": -60,
"battery": 100,
"errors": [],
},
}

View File

@ -0,0 +1,48 @@
# serializer version: 1
# name: test_blu_trv_binary_sensor_entity[binary_sensor.trv_name_calibration-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.trv_name_calibration',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
'original_icon': None,
'original_name': 'TRV-Name calibration',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC-blutrv:200-calibration',
'unit_of_measurement': None,
})
# ---
# name: test_blu_trv_binary_sensor_entity[binary_sensor.trv_name_calibration-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'problem',
'friendly_name': 'TRV-Name calibration',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.trv_name_calibration',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,113 @@
# serializer version: 1
# name: test_blu_trv_number_entity[number.trv_name_external_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 50,
'min': -50,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'number.trv_name_external_temperature',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'TRV-Name external temperature',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'external_temperature',
'unique_id': '123456789ABC-blutrv:200-external_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_blu_trv_number_entity[number.trv_name_external_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name external temperature',
'max': 50,
'min': -50,
'mode': <NumberMode.BOX: 'box'>,
'step': 0.1,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'number.trv_name_external_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '15.2',
})
# ---
# name: test_blu_trv_number_entity[number.trv_name_valve_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'max': 100,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'number',
'entity_category': None,
'entity_id': 'number.trv_name_valve_position',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'TRV-Name valve position',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '123456789ABC-blutrv:200-valve_position',
'unit_of_measurement': '%',
})
# ---
# name: test_blu_trv_number_entity[number.trv_name_valve_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name valve position',
'max': 100,
'min': 0,
'mode': <NumberMode.SLIDER: 'slider'>,
'step': 1,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'number.trv_name_valve_position',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---

View File

@ -0,0 +1,153 @@
# serializer version: 1
# name: test_blu_trv_sensor_entity[sensor.trv_name_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_battery',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.BATTERY: 'battery'>,
'original_icon': None,
'original_name': 'TRV-Name battery',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC-blutrv:200-blutrv_battery',
'unit_of_measurement': '%',
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_battery-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'battery',
'friendly_name': 'TRV-Name battery',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.trv_name_battery',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '100',
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_signal_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_signal_strength',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.SIGNAL_STRENGTH: 'signal_strength'>,
'original_icon': None,
'original_name': 'TRV-Name signal strength',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '123456789ABC-blutrv:200-blutrv_rssi',
'unit_of_measurement': 'dBm',
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_signal_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'signal_strength',
'friendly_name': 'TRV-Name signal strength',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'dBm',
}),
'context': <ANY>,
'entity_id': 'sensor.trv_name_signal_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '-60',
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'sensor.trv_name_valve_position',
'has_entity_name': False,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'TRV-Name valve position',
'platform': 'shelly',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'valve_position',
'unique_id': '123456789ABC-blutrv:200-valve_position',
'unit_of_measurement': '%',
})
# ---
# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'TRV-Name valve position',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.trv_name_valve_position',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---

View File

@ -3,9 +3,10 @@
from copy import deepcopy
from unittest.mock import Mock
from aioshelly.const import MODEL_MOTION
from aioshelly.const import MODEL_BLU_GATEWAY_GEN3, MODEL_MOTION
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.shelly.const import UPDATE_PERIOD_MULTIPLIER
@ -477,3 +478,22 @@ async def test_rpc_remove_virtual_binary_sensor_when_orphaned(
entry = entity_registry.async_get(entity_id)
assert not entry
async def test_blu_trv_binary_sensor_entity(
hass: HomeAssistant,
mock_blu_trv: Mock,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test BLU TRV binary sensor entity."""
await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3)
for entity in ("calibration",):
entity_id = f"{BINARY_SENSOR_DOMAIN}.trv_name_{entity}"
state = hass.states.get(entity_id)
assert state == snapshot(name=f"{entity_id}-state")
entry = entity_registry.async_get(entity_id)
assert entry == snapshot(name=f"{entity_id}-entry")

View File

@ -3,8 +3,10 @@
from copy import deepcopy
from unittest.mock import AsyncMock, Mock
from aioshelly.const import MODEL_BLU_GATEWAY_GEN3
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.number import (
ATTR_MAX,
@ -390,3 +392,26 @@ async def test_rpc_remove_virtual_number_when_orphaned(
entry = entity_registry.async_get(entity_id)
assert not entry
async def test_blu_trv_number_entity(
hass: HomeAssistant,
mock_blu_trv: Mock,
entity_registry: EntityRegistry,
monkeypatch: pytest.MonkeyPatch,
snapshot: SnapshotAssertion,
) -> None:
"""Test BLU TRV number entity."""
# disable automatic temperature control in the device
monkeypatch.setitem(mock_blu_trv.config["blutrv:200"], "enable", False)
await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3)
for entity in ("external_temperature", "valve_position"):
entity_id = f"{NUMBER_DOMAIN}.trv_name_{entity}"
state = hass.states.get(entity_id)
assert state == snapshot(name=f"{entity_id}-state")
entry = entity_registry.async_get(entity_id)
assert entry == snapshot(name=f"{entity_id}-entry")

View File

@ -3,8 +3,10 @@
from copy import deepcopy
from unittest.mock import Mock
from aioshelly.const import MODEL_BLU_GATEWAY_GEN3
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.homeassistant import (
DOMAIN as HA_DOMAIN,
@ -1405,3 +1407,22 @@ async def test_rpc_voltmeter_value(
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.unique_id == "123456789ABC-voltmeter:100-voltmeter_value"
async def test_blu_trv_sensor_entity(
hass: HomeAssistant,
mock_blu_trv: Mock,
entity_registry: EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test BLU TRV sensor entity."""
await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_GEN3)
for entity in ("battery", "signal_strength", "valve_position"):
entity_id = f"{SENSOR_DOMAIN}.trv_name_{entity}"
state = hass.states.get(entity_id)
assert state == snapshot(name=f"{entity_id}-state")
entry = entity_registry.async_get(entity_id)
assert entry == snapshot(name=f"{entity_id}-entry")