From b38a614170eb6f73f0f2911c5175e5cd27944458 Mon Sep 17 00:00:00 2001 From: dotvav Date: Fri, 22 Nov 2024 12:53:39 +0100 Subject: [PATCH] Palazzetti sensors (#130804) --- .../components/palazzetti/__init__.py | 2 +- .../components/palazzetti/climate.py | 17 +- homeassistant/components/palazzetti/entity.py | 27 ++ homeassistant/components/palazzetti/sensor.py | 106 +++++ .../components/palazzetti/strings.json | 29 ++ tests/components/palazzetti/conftest.py | 39 ++ .../palazzetti/snapshots/test_sensor.ambr | 409 ++++++++++++++++++ tests/components/palazzetti/test_sensor.py | 27 ++ 8 files changed, 641 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/palazzetti/entity.py create mode 100644 homeassistant/components/palazzetti/sensor.py create mode 100644 tests/components/palazzetti/snapshots/test_sensor.ambr create mode 100644 tests/components/palazzetti/test_sensor.py diff --git a/homeassistant/components/palazzetti/__init__.py b/homeassistant/components/palazzetti/__init__.py index ecaa8089097..4bea4434496 100644 --- a/homeassistant/components/palazzetti/__init__.py +++ b/homeassistant/components/palazzetti/__init__.py @@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant from .coordinator import PalazzettiConfigEntry, PalazzettiDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.CLIMATE] +PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: PalazzettiConfigEntry) -> bool: diff --git a/homeassistant/components/palazzetti/climate.py b/homeassistant/components/palazzetti/climate.py index 055b3b40172..95e267301bc 100644 --- a/homeassistant/components/palazzetti/climate.py +++ b/homeassistant/components/palazzetti/climate.py @@ -13,13 +13,12 @@ from homeassistant.components.climate import ( from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError -from homeassistant.helpers import device_registry as dr from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import PalazzettiConfigEntry -from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES, FAN_SILENT, PALAZZETTI +from .const import DOMAIN, FAN_AUTO, FAN_HIGH, FAN_MODES, FAN_SILENT from .coordinator import PalazzettiDataUpdateCoordinator +from .entity import PalazzettiEntity async def async_setup_entry( @@ -31,9 +30,7 @@ async def async_setup_entry( async_add_entities([PalazzettiClimateEntity(entry.runtime_data)]) -class PalazzettiClimateEntity( - CoordinatorEntity[PalazzettiDataUpdateCoordinator], ClimateEntity -): +class PalazzettiClimateEntity(PalazzettiEntity, ClimateEntity): """Defines a Palazzetti climate.""" _attr_has_entity_name = True @@ -53,15 +50,7 @@ class PalazzettiClimateEntity( super().__init__(coordinator) client = coordinator.client mac = coordinator.config_entry.unique_id - assert mac is not None self._attr_unique_id = mac - self._attr_device_info = dr.DeviceInfo( - connections={(dr.CONNECTION_NETWORK_MAC, mac)}, - name=client.name, - manufacturer=PALAZZETTI, - sw_version=client.sw_version, - hw_version=client.hw_version, - ) self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] self._attr_min_temp = client.target_temperature_min self._attr_max_temp = client.target_temperature_max diff --git a/homeassistant/components/palazzetti/entity.py b/homeassistant/components/palazzetti/entity.py new file mode 100644 index 00000000000..ec850848154 --- /dev/null +++ b/homeassistant/components/palazzetti/entity.py @@ -0,0 +1,27 @@ +"""Base class for Palazzetti entities.""" + +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import PALAZZETTI +from .coordinator import PalazzettiDataUpdateCoordinator + + +class PalazzettiEntity(CoordinatorEntity[PalazzettiDataUpdateCoordinator]): + """Defines a base Palazzetti entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: PalazzettiDataUpdateCoordinator) -> None: + """Initialize Palazzetti entity.""" + super().__init__(coordinator) + client = coordinator.client + mac = coordinator.config_entry.unique_id + assert mac is not None + self._attr_device_info = dr.DeviceInfo( + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, + name=client.name, + manufacturer=PALAZZETTI, + sw_version=client.sw_version, + hw_version=client.hw_version, + ) diff --git a/homeassistant/components/palazzetti/sensor.py b/homeassistant/components/palazzetti/sensor.py new file mode 100644 index 00000000000..ead2b236b17 --- /dev/null +++ b/homeassistant/components/palazzetti/sensor.py @@ -0,0 +1,106 @@ +"""Support for Palazzetti sensors.""" + +from dataclasses import dataclass + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfLength, UnitOfMass, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType + +from . import PalazzettiConfigEntry +from .coordinator import PalazzettiDataUpdateCoordinator +from .entity import PalazzettiEntity + + +@dataclass(frozen=True, kw_only=True) +class PropertySensorEntityDescription(SensorEntityDescription): + """Describes a Palazzetti sensor entity that is read from a `PalazzettiClient` property.""" + + client_property: str + presence_flag: None | str = None + + +PROPERTY_SENSOR_DESCRIPTIONS: list[PropertySensorEntityDescription] = [ + PropertySensorEntityDescription( + key="pellet_quantity", + device_class=SensorDeviceClass.WEIGHT, + native_unit_of_measurement=UnitOfMass.KILOGRAMS, + state_class=SensorStateClass.MEASUREMENT, + translation_key="pellet_quantity", + client_property="pellet_quantity", + ), + PropertySensorEntityDescription( + key="pellet_level", + device_class=SensorDeviceClass.DISTANCE, + native_unit_of_measurement=UnitOfLength.CENTIMETERS, + state_class=SensorStateClass.MEASUREMENT, + translation_key="pellet_level", + presence_flag="has_pellet_level", + client_property="pellet_level", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PalazzettiConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Palazzetti sensor entities based on a config entry.""" + + coordinator = entry.runtime_data + + sensors = [ + PalazzettiSensor( + coordinator, + PropertySensorEntityDescription( + key=sensor.description_key.value, + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + translation_key=sensor.description_key.value, + client_property=sensor.state_property, + ), + ) + for sensor in coordinator.client.list_temperatures() + ] + + sensors.extend( + [ + PalazzettiSensor(coordinator, description) + for description in PROPERTY_SENSOR_DESCRIPTIONS + if not description.presence_flag + or getattr(coordinator.client, description.presence_flag) + ] + ) + + if sensors: + async_add_entities(sensors) + + +class PalazzettiSensor(PalazzettiEntity, SensorEntity): + """Define a Palazzetti sensor.""" + + entity_description: PropertySensorEntityDescription + + def __init__( + self, + coordinator: PalazzettiDataUpdateCoordinator, + description: PropertySensorEntityDescription, + ) -> None: + """Initialize Palazzetti sensor.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{description.key}" + + @property + def native_value(self) -> StateType: + """Return the state value of the sensor.""" + + return getattr(self.coordinator.client, self.entity_description.client_property) diff --git a/homeassistant/components/palazzetti/strings.json b/homeassistant/components/palazzetti/strings.json index cc10c8ed5c6..718a21f1889 100644 --- a/homeassistant/components/palazzetti/strings.json +++ b/homeassistant/components/palazzetti/strings.json @@ -47,6 +47,35 @@ } } } + }, + "sensor": { + "pellet_quantity": { + "name": "Pellet quantity" + }, + "pellet_level": { + "name": "Pellet level" + }, + "air_outlet_temperature": { + "name": "Air outlet temperature" + }, + "wood_combustion_temperature": { + "name": "Wood combustion temperature" + }, + "room_temperature": { + "name": "Room temperature" + }, + "return_water_temperature": { + "name": "Return water temperature" + }, + "tank_water_temperature": { + "name": "Tank water temperature" + }, + "t1_hydro": { + "name": "Hydro temperature 1" + }, + "t2_hydro": { + "name": "Hydro temperature 2" + } } } } diff --git a/tests/components/palazzetti/conftest.py b/tests/components/palazzetti/conftest.py index 33dca845098..b36a58879c1 100644 --- a/tests/components/palazzetti/conftest.py +++ b/tests/components/palazzetti/conftest.py @@ -3,6 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch +from pypalazzetti.temperature import TemperatureDefinition, TemperatureDescriptionKey import pytest from homeassistant.components.palazzetti.const import DOMAIN @@ -56,12 +57,20 @@ def mock_palazzetti_client() -> Generator[AsyncMock]: mock_client.has_fan_high = True mock_client.has_fan_auto = True mock_client.has_on_off_switch = True + mock_client.has_pellet_level = False mock_client.connected = True mock_client.is_heating = True mock_client.room_temperature = 18 + mock_client.T1 = 21.5 + mock_client.T2 = 25.1 + mock_client.T3 = 45 + mock_client.T4 = 0 + mock_client.T5 = 0 mock_client.target_temperature = 21 mock_client.target_temperature_min = 5 mock_client.target_temperature_max = 50 + mock_client.pellet_quantity = 1248 + mock_client.pellet_level = 0 mock_client.fan_speed = 3 mock_client.connect.return_value = True mock_client.update_state.return_value = True @@ -71,4 +80,34 @@ def mock_palazzetti_client() -> Generator[AsyncMock]: mock_client.set_fan_silent.return_value = True mock_client.set_fan_high.return_value = True mock_client.set_fan_auto.return_value = True + mock_client.list_temperatures.return_value = [ + TemperatureDefinition( + description_key=TemperatureDescriptionKey.ROOM_TEMP, + state_property="T1", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.RETURN_WATER_TEMP, + state_property="T4", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.TANK_WATER_TEMP, + state_property="T5", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.WOOD_COMBUSTION_TEMP, + state_property="T3", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.AIR_OUTLET_TEMP, + state_property="T2", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.T1_HYDRO_TEMP, + state_property="T1", + ), + TemperatureDefinition( + description_key=TemperatureDescriptionKey.T2_HYDRO_TEMP, + state_property="T2", + ), + ] yield mock_client diff --git a/tests/components/palazzetti/snapshots/test_sensor.ambr b/tests/components/palazzetti/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..107b818f195 --- /dev/null +++ b/tests/components/palazzetti/snapshots/test_sensor.ambr @@ -0,0 +1,409 @@ +# serializer version: 1 +# name: test_all_entities[sensor.stove_air_outlet_temperature-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': None, + 'entity_id': 'sensor.stove_air_outlet_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air outlet temperature', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'air_outlet_temperature', + 'unique_id': '11:22:33:44:55:66-air_outlet_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_air_outlet_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Air outlet temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_air_outlet_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.1', + }) +# --- +# name: test_all_entities[sensor.stove_hydro_temperature_1-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': None, + 'entity_id': 'sensor.stove_hydro_temperature_1', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hydro temperature 1', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 't1_hydro', + 'unique_id': '11:22:33:44:55:66-t1_hydro', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_hydro_temperature_1-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Hydro temperature 1', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_hydro_temperature_1', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.5', + }) +# --- +# name: test_all_entities[sensor.stove_hydro_temperature_2-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': None, + 'entity_id': 'sensor.stove_hydro_temperature_2', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hydro temperature 2', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 't2_hydro', + 'unique_id': '11:22:33:44:55:66-t2_hydro', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_hydro_temperature_2-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Hydro temperature 2', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_hydro_temperature_2', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '25.1', + }) +# --- +# name: test_all_entities[sensor.stove_pellet_quantity-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': None, + 'entity_id': 'sensor.stove_pellet_quantity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Pellet quantity', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'pellet_quantity', + 'unique_id': '11:22:33:44:55:66-pellet_quantity', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_pellet_quantity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'weight', + 'friendly_name': 'Stove Pellet quantity', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_pellet_quantity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1248', + }) +# --- +# name: test_all_entities[sensor.stove_return_water_temperature-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': None, + 'entity_id': 'sensor.stove_return_water_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Return water temperature', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'return_water_temperature', + 'unique_id': '11:22:33:44:55:66-return_water_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_return_water_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Return water temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_return_water_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.stove_room_temperature-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': None, + 'entity_id': 'sensor.stove_room_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Room temperature', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'room_temperature', + 'unique_id': '11:22:33:44:55:66-room_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_room_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Room temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_room_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.5', + }) +# --- +# name: test_all_entities[sensor.stove_tank_water_temperature-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': None, + 'entity_id': 'sensor.stove_tank_water_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tank water temperature', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'tank_water_temperature', + 'unique_id': '11:22:33:44:55:66-tank_water_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_tank_water_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Tank water temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_tank_water_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.stove_wood_combustion_temperature-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': None, + 'entity_id': 'sensor.stove_wood_combustion_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Wood combustion temperature', + 'platform': 'palazzetti', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'wood_combustion_temperature', + 'unique_id': '11:22:33:44:55:66-wood_combustion_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.stove_wood_combustion_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Stove Wood combustion temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.stove_wood_combustion_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '45', + }) +# --- diff --git a/tests/components/palazzetti/test_sensor.py b/tests/components/palazzetti/test_sensor.py new file mode 100644 index 00000000000..c7d7317bb0b --- /dev/null +++ b/tests/components/palazzetti/test_sensor.py @@ -0,0 +1,27 @@ +"""Tests for the Palazzetti sensor platform.""" + +from unittest.mock import AsyncMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_palazzetti_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.palazzetti.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)