From 37675b32fcd95f5cf77df6d0e299c1dd48f1af53 Mon Sep 17 00:00:00 2001 From: Trond Elde Date: Fri, 11 Sep 2026 12:19:41 +0200 Subject: [PATCH] Add Flexit sensor entities (#181882) --- homeassistant/components/flexit/__init__.py | 6 +- homeassistant/components/flexit/sensor.py | 117 ++++++++ homeassistant/components/flexit/strings.json | 17 ++ .../flexit/snapshots/test_sensor.ambr | 279 ++++++++++++++++++ tests/components/flexit/test_sensor.py | 43 +++ 5 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/flexit/sensor.py create mode 100644 tests/components/flexit/snapshots/test_sensor.ambr create mode 100644 tests/components/flexit/test_sensor.py diff --git a/homeassistant/components/flexit/__init__.py b/homeassistant/components/flexit/__init__.py index dd8ba1ca7ae4..bf848b74b247 100644 --- a/homeassistant/components/flexit/__init__.py +++ b/homeassistant/components/flexit/__init__.py @@ -12,7 +12,11 @@ from homeassistant.core import HomeAssistant from .const import CONF_BAUDRATE, CONF_UNIT, DEFAULT_PORT, TYPE_SERIAL from .coordinator import FlexitConfigEntry, FlexitDataCoordinator -_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.CLIMATE] +_PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.CLIMATE, + Platform.SENSOR, +] def create_modbus_params( diff --git a/homeassistant/components/flexit/sensor.py b/homeassistant/components/flexit/sensor.py new file mode 100644 index 000000000000..89256f051f64 --- /dev/null +++ b/homeassistant/components/flexit/sensor.py @@ -0,0 +1,117 @@ +"""Sensor platform for the Flexit integration.""" + +from collections.abc import Callable +from dataclasses import dataclass +from typing import override + +from flexit_modbus import Measurements + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + PERCENTAGE, + EntityCategory, + UnitOfTemperature, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import FlexitConfigEntry, FlexitDataCoordinator +from .entity import FlexitEntity + + +@dataclass(kw_only=True, frozen=True) +class FlexitSensorEntityDescription(SensorEntityDescription): + """Describe a Flexit sensor entity.""" + + value_fn: Callable[[Measurements], StateType] + + +SENSORS: tuple[FlexitSensorEntityDescription, ...] = ( + FlexitSensorEntityDescription( + key="air_filter_operating_time", + translation_key="air_filter_operating_time", + device_class=SensorDeviceClass.DURATION, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTime.HOURS, + state_class=SensorStateClass.TOTAL_INCREASING, + suggested_display_precision=0, + value_fn=lambda measurements: measurements.filter_running_hours, + ), + FlexitSensorEntityDescription( + key="heat_exchanger_regulation", + translation_key="heat_exchanger_regulation", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda measurements: measurements.heat_exchanger_regulation, + ), + FlexitSensorEntityDescription( + key="electric_heater_regulation", + translation_key="electric_heater_regulation", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda measurements: measurements.electric_heater_regulation, + ), + FlexitSensorEntityDescription( + key="cooling_regulation", + translation_key="cooling_regulation", + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda measurements: measurements.cooling_regulation, + ), + FlexitSensorEntityDescription( + key="outdoor_air_temperature", + translation_key="outdoor_air_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + entity_category=EntityCategory.DIAGNOSTIC, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + value_fn=lambda measurements: measurements.outdoor_air_temperature, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: FlexitConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Flexit sensor entities.""" + coordinator = entry.runtime_data + async_add_entities( + FlexitSensor(coordinator, description) for description in SENSORS + ) + + +class FlexitSensor(FlexitEntity, SensorEntity): + """Representation of a Flexit sensor.""" + + entity_description: FlexitSensorEntityDescription + + def __init__( + self, + coordinator: FlexitDataCoordinator, + entity_description: FlexitSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" + assert coordinator.config_entry is not None + super().__init__(coordinator) + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.entry_id}-{entity_description.key}" + ) + + @property + @override + def native_value(self) -> StateType: + """Return the sensor value.""" + return self.entity_description.value_fn(self.coordinator.device.measurements) diff --git a/homeassistant/components/flexit/strings.json b/homeassistant/components/flexit/strings.json index 732ccbeeda05..bc570060c234 100644 --- a/homeassistant/components/flexit/strings.json +++ b/homeassistant/components/flexit/strings.json @@ -64,6 +64,23 @@ "filter_alarm": { "name": "Filter alarm" } + }, + "sensor": { + "air_filter_operating_time": { + "name": "Air filter operating time" + }, + "cooling_regulation": { + "name": "Cooling regulation" + }, + "electric_heater_regulation": { + "name": "Electric heater regulation" + }, + "heat_exchanger_regulation": { + "name": "Heat exchanger regulation" + }, + "outdoor_air_temperature": { + "name": "Outdoor air temperature" + } } }, "issues": { diff --git a/tests/components/flexit/snapshots/test_sensor.ambr b/tests/components/flexit/snapshots/test_sensor.ambr new file mode 100644 index 000000000000..8471b428398d --- /dev/null +++ b/tests/components/flexit/snapshots/test_sensor.ambr @@ -0,0 +1,279 @@ +# serializer version: 1 +# name: test_sensors[sensor.flexit_air_filter_operating_time-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.flexit_air_filter_operating_time', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Air filter operating time', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Air filter operating time', + 'platform': 'flexit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'air_filter_operating_time', + 'unique_id': 'flexit_001-air_filter_operating_time', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.flexit_air_filter_operating_time-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'duration', + : 'Flexit Air filter operating time', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.flexit_air_filter_operating_time', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120', + }) +# --- +# name: test_sensors[sensor.flexit_cooling_regulation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.flexit_cooling_regulation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Cooling regulation', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Cooling regulation', + 'platform': 'flexit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'cooling_regulation', + 'unique_id': 'flexit_001-cooling_regulation', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.flexit_cooling_regulation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Flexit Cooling regulation', + : , + : '%', + }), + 'context': , + 'entity_id': 'sensor.flexit_cooling_regulation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_sensors[sensor.flexit_electric_heater_regulation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.flexit_electric_heater_regulation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Electric heater regulation', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Electric heater regulation', + 'platform': 'flexit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'electric_heater_regulation', + 'unique_id': 'flexit_001-electric_heater_regulation', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.flexit_electric_heater_regulation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Flexit Electric heater regulation', + : , + : '%', + }), + 'context': , + 'entity_id': 'sensor.flexit_electric_heater_regulation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_sensors[sensor.flexit_heat_exchanger_regulation-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.flexit_heat_exchanger_regulation', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Heat exchanger regulation', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Heat exchanger regulation', + 'platform': 'flexit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'heat_exchanger_regulation', + 'unique_id': 'flexit_001-heat_exchanger_regulation', + 'unit_of_measurement': '%', + }) +# --- +# name: test_sensors[sensor.flexit_heat_exchanger_regulation-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'Flexit Heat exchanger regulation', + : , + : '%', + }), + 'context': , + 'entity_id': 'sensor.flexit_heat_exchanger_regulation', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '35', + }) +# --- +# name: test_sensors[sensor.flexit_outdoor_air_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + : , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': , + 'entity_id': 'sensor.flexit_outdoor_air_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Outdoor air temperature', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Outdoor air temperature', + 'platform': 'flexit', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'outdoor_air_temperature', + 'unique_id': 'flexit_001-outdoor_air_temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_sensors[sensor.flexit_outdoor_air_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'temperature', + : 'Flexit Outdoor air temperature', + : , + : , + }), + 'context': , + 'entity_id': 'sensor.flexit_outdoor_air_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5.0', + }) +# --- diff --git a/tests/components/flexit/test_sensor.py b/tests/components/flexit/test_sensor.py new file mode 100644 index 000000000000..968dcc51da42 --- /dev/null +++ b/tests/components/flexit/test_sensor.py @@ -0,0 +1,43 @@ +"""Test the Flexit sensor platform.""" + +from unittest.mock import patch + +from modbus_connection.mock import MockModbusUnit +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_sensors( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + mock_modbus_unit: MockModbusUnit, +) -> None: + """Test Flexit sensor states.""" + mock_modbus_unit.holding.update({8: 215, 17: 2}) + mock_modbus_unit.input.update( + { + 8: 120, + 9: 200, + 11: 50, + 13: 20, + 14: 35, + 15: 40, + 27: 0, + 28: 1, + 48: 10, + } + ) + mock_config_entry.add_to_hass(hass) + + with patch("homeassistant.components.flexit._PLATFORMS", [Platform.SENSOR]): + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)