diff --git a/homeassistant/components/shelly/binary_sensor.py b/homeassistant/components/shelly/binary_sensor.py index 556274aa51a4f..108a823673331 100644 --- a/homeassistant/components/shelly/binary_sensor.py +++ b/homeassistant/components/shelly/binary_sensor.py @@ -15,11 +15,12 @@ ) 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 @@ class RestBinarySensorDescription(RestEntityDescription, BinarySensorEntityDescr 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 @@ def is_on(self) -> bool: 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 ): diff --git a/homeassistant/components/shelly/entity.py b/homeassistant/components/shelly/entity.py index aea060e09e2ed..8c9044aeaff37 100644 --- a/homeassistant/components/shelly/entity.py +++ b/homeassistant/components/shelly/entity.py @@ -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 @@ async def async_update(self) -> None: "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 diff --git a/homeassistant/components/shelly/icons.json b/homeassistant/components/shelly/icons.json index 1baf61acf3ba8..f93abf6b85403 100644 --- a/homeassistant/components/shelly/icons.json +++ b/homeassistant/components/shelly/icons.json @@ -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" } diff --git a/homeassistant/components/shelly/number.py b/homeassistant/components/shelly/number.py index 2aed38fb72304..fb61c885423ca 100644 --- a/homeassistant/components/shelly/number.py +++ b/homeassistant/components/shelly/number.py @@ -18,9 +18,10 @@ 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 @@ class RpcNumberDescription(RpcEntityDescription, NumberEntityDescription): 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 @@ class RpcNumberDescription(RpcEntityDescription, NumberEntityDescription): 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 @@ async def _set_state_full_path(self, path: str, params: Any) -> Any: ) 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}) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index 139a427f0870f..6d000556cf371 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -33,6 +33,7 @@ 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 @@ class RestSensorDescription(RestEntityDescription, SensorEntityDescription): 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 @@ def native_value(self) -> StateType: 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.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 7bcc1c04c6a15..85cd558e918fe 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -255,6 +255,8 @@ def mock_white_light_set_state( "current_C": 15.2, "target_C": 17.1, "schedule_rev": 0, + "rssi": -60, + "battery": 100, "errors": [], }, } diff --git a/tests/components/shelly/snapshots/test_binary_sensor.ambr b/tests/components/shelly/snapshots/test_binary_sensor.ambr new file mode 100644 index 0000000000000..8dcb7b00a420e --- /dev/null +++ b/tests/components/shelly/snapshots/test_binary_sensor.ambr @@ -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': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.trv_name_calibration', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'entity_id': 'binary_sensor.trv_name_calibration', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- diff --git a/tests/components/shelly/snapshots/test_number.ambr b/tests/components/shelly/snapshots/test_number.ambr new file mode 100644 index 0000000000000..965d44698c27a --- /dev/null +++ b/tests/components/shelly/snapshots/test_number.ambr @@ -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': , + 'step': 0.1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.trv_name_external_temperature', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + }) +# --- +# 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': , + 'step': 0.1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.trv_name_external_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + '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': , + 'step': 1, + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + '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': , + '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': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.trv_name_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/shelly/snapshots/test_sensor.ambr b/tests/components/shelly/snapshots/test_sensor.ambr new file mode 100644 index 0000000000000..8ab767ca889c2 --- /dev/null +++ b/tests/components/shelly/snapshots/test_sensor.ambr @@ -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': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_battery', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.trv_name_battery', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_signal_strength-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_signal_strength', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + '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': , + 'unit_of_measurement': 'dBm', + }), + 'context': , + 'entity_id': 'sensor.trv_name_signal_strength', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-60', + }) +# --- +# name: test_blu_trv_sensor_entity[sensor.trv_name_valve_position-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.trv_name_valve_position', + 'has_entity_name': False, + 'hidden_by': None, + 'icon': None, + 'id': , + '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': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.trv_name_valve_position', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- diff --git a/tests/components/shelly/test_binary_sensor.py b/tests/components/shelly/test_binary_sensor.py index fadfe28db3e45..ed36a43a55695 100644 --- a/tests/components/shelly/test_binary_sensor.py +++ b/tests/components/shelly/test_binary_sensor.py @@ -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") diff --git a/tests/components/shelly/test_number.py b/tests/components/shelly/test_number.py index 6c1cc394b64fc..2a64ab839ea3a 100644 --- a/tests/components/shelly/test_number.py +++ b/tests/components/shelly/test_number.py @@ -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") diff --git a/tests/components/shelly/test_sensor.py b/tests/components/shelly/test_sensor.py index c62f21d9c8f98..0bbb374012f9f 100644 --- a/tests/components/shelly/test_sensor.py +++ b/tests/components/shelly/test_sensor.py @@ -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")