From 254aa30ad8ff060792fdabea616013fa06f7355c Mon Sep 17 00:00:00 2001 From: prana-dev-official Date: Mon, 16 Mar 2026 21:03:36 +0200 Subject: [PATCH] Add sensor platform to prana (#165632) --- homeassistant/components/prana/__init__.py | 2 +- homeassistant/components/prana/icons.json | 14 ++ homeassistant/components/prana/sensor.py | 129 ++++++++++++++++++ homeassistant/components/prana/strings.json | 14 ++ .../prana/snapshots/test_sensor.ambr | 112 +++++++++++++++ tests/components/prana/test_sensor.py | 44 ++++++ 6 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/prana/sensor.py create mode 100644 tests/components/prana/snapshots/test_sensor.ambr create mode 100644 tests/components/prana/test_sensor.py diff --git a/homeassistant/components/prana/__init__.py b/homeassistant/components/prana/__init__.py index 1fdf92be64b..abe2013ca52 100644 --- a/homeassistant/components/prana/__init__.py +++ b/homeassistant/components/prana/__init__.py @@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator _LOGGER = logging.getLogger(__name__) -PLATFORMS = [Platform.FAN, Platform.SWITCH] +PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH] async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool: diff --git a/homeassistant/components/prana/icons.json b/homeassistant/components/prana/icons.json index 5002c12e208..799291adbed 100644 --- a/homeassistant/components/prana/icons.json +++ b/homeassistant/components/prana/icons.json @@ -8,6 +8,20 @@ "default": "mdi:arrow-expand-left" } }, + "sensor": { + "inside_temperature": { + "default": "mdi:home-thermometer" + }, + "inside_temperature_2": { + "default": "mdi:home-thermometer" + }, + "outside_temperature": { + "default": "mdi:thermometer" + }, + "outside_temperature_2": { + "default": "mdi:thermometer" + } + }, "switch": { "auto": { "default": "mdi:fan-auto" diff --git a/homeassistant/components/prana/sensor.py b/homeassistant/components/prana/sensor.py new file mode 100644 index 00000000000..6d45cd39b56 --- /dev/null +++ b/homeassistant/components/prana/sensor.py @@ -0,0 +1,129 @@ +"""Sensor platform for Prana integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from enum import StrEnum + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, + StateType, +) +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_BILLION, + CONCENTRATION_PARTS_PER_MILLION, + PERCENTAGE, + UnitOfPressure, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import PranaConfigEntry +from .entity import PranaBaseEntity, PranaCoordinator + +PARALLEL_UPDATES = 1 + + +class PranaSensorType(StrEnum): + """Enumerates Prana sensor types exposed by the device API.""" + + HUMIDITY = "humidity" + VOC = "voc" + AIR_PRESSURE = "air_pressure" + CO2 = "co2" + INSIDE_TEMPERATURE = "inside_temperature" + INSIDE_TEMPERATURE_2 = "inside_temperature_2" + OUTSIDE_TEMPERATURE = "outside_temperature" + OUTSIDE_TEMPERATURE_2 = "outside_temperature_2" + + +@dataclass(frozen=True, kw_only=True) +class PranaSensorEntityDescription(SensorEntityDescription): + """Description of a Prana sensor entity.""" + + key: PranaSensorType + state_class: SensorStateClass = SensorStateClass.MEASUREMENT + value_fn: Callable[[PranaCoordinator], StateType | None] + + +ENTITIES: tuple[PranaSensorEntityDescription, ...] = ( + PranaSensorEntityDescription( + key=PranaSensorType.HUMIDITY, + value_fn=lambda coord: coord.data.humidity, + native_unit_of_measurement=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + ), + PranaSensorEntityDescription( + key=PranaSensorType.VOC, + value_fn=lambda coord: coord.data.voc, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS, + ), + PranaSensorEntityDescription( + key=PranaSensorType.AIR_PRESSURE, + value_fn=lambda coord: coord.data.air_pressure, + native_unit_of_measurement=UnitOfPressure.MMHG, + device_class=SensorDeviceClass.PRESSURE, + ), + PranaSensorEntityDescription( + key=PranaSensorType.CO2, + value_fn=lambda coord: coord.data.co2, + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=SensorDeviceClass.CO2, + ), + PranaSensorEntityDescription( + key=PranaSensorType.INSIDE_TEMPERATURE, + translation_key="inside_temperature", + value_fn=lambda coord: coord.data.inside_temperature, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PranaSensorEntityDescription( + key=PranaSensorType.INSIDE_TEMPERATURE_2, + translation_key="inside_temperature_2", + value_fn=lambda coord: coord.data.inside_temperature_2, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PranaSensorEntityDescription( + key=PranaSensorType.OUTSIDE_TEMPERATURE, + translation_key="outside_temperature", + value_fn=lambda coord: coord.data.outside_temperature, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), + PranaSensorEntityDescription( + key=PranaSensorType.OUTSIDE_TEMPERATURE_2, + translation_key="outside_temperature_2", + value_fn=lambda coord: coord.data.outside_temperature_2, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: PranaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Prana sensor entities from a config entry.""" + async_add_entities( + PranaSensor(entry.runtime_data, description) + for description in ENTITIES + if description.value_fn(entry.runtime_data) is not None + ) + + +class PranaSensor(PranaBaseEntity, SensorEntity): + """Representation of a Prana sensor entity.""" + + entity_description: PranaSensorEntityDescription + + @property + def native_value(self) -> StateType | None: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator) diff --git a/homeassistant/components/prana/strings.json b/homeassistant/components/prana/strings.json index 646ec1d3d39..1163fed4468 100644 --- a/homeassistant/components/prana/strings.json +++ b/homeassistant/components/prana/strings.json @@ -49,6 +49,20 @@ } } }, + "sensor": { + "inside_temperature": { + "name": "Inside temperature" + }, + "inside_temperature_2": { + "name": "Inside temperature 2" + }, + "outside_temperature": { + "name": "Outside temperature" + }, + "outside_temperature_2": { + "name": "Outside temperature 2" + } + }, "switch": { "auto": { "name": "Auto" diff --git a/tests/components/prana/snapshots/test_sensor.ambr b/tests/components/prana/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..2337395eb0f --- /dev/null +++ b/tests/components/prana/snapshots/test_sensor.ambr @@ -0,0 +1,112 @@ +# serializer version: 1 +# name: test_sensors[sensor.prana_recuperator_humidity-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.prana_recuperator_humidity', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Humidity', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Humidity', + 'platform': 'prana', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'ECC9FFE0E574_humidity', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.prana_recuperator_humidity-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'humidity', + 'friendly_name': 'PRANA RECUPERATOR Humidity', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.prana_recuperator_humidity', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '56', + }) +# --- +# name: test_sensors[sensor.prana_recuperator_inside_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.prana_recuperator_inside_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Inside temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Inside temperature', + 'platform': 'prana', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'inside_temperature', + 'unique_id': 'ECC9FFE0E574_inside_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.prana_recuperator_inside_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'PRANA RECUPERATOR Inside temperature', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.prana_recuperator_inside_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '21.7', + }) +# --- diff --git a/tests/components/prana/test_sensor.py b/tests/components/prana/test_sensor.py new file mode 100644 index 00000000000..715008651b9 --- /dev/null +++ b/tests/components/prana/test_sensor.py @@ -0,0 +1,44 @@ +"""Tests for the Prana sensor platform.""" + +from unittest.mock import MagicMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import async_init_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + mock_prana_api: MagicMock, + snapshot: SnapshotAssertion, +) -> None: + """Test all Prana sensors using snapshots.""" + with patch("homeassistant.components.prana.PLATFORMS", [Platform.SENSOR]): + await async_init_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_sensors_not_added_if_none( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_prana_api: MagicMock, +) -> None: + """Test sensors are not added when value_fn returns None.""" + + mock_prana_api.get_state.return_value.co2 = None + mock_prana_api.get_state.return_value.humidity = 45 + + with patch("homeassistant.components.prana.PLATFORMS", [Platform.SENSOR]): + await async_init_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.prana_recuperator_humidity") is not None + assert hass.states.get("sensor.prana_recuperator_co2") is None