diff --git a/homeassistant/components/flexit_bacnet/__init__.py b/homeassistant/components/flexit_bacnet/__init__.py index ba7134d7e50..5732fb3822c 100644 --- a/homeassistant/components/flexit_bacnet/__init__.py +++ b/homeassistant/components/flexit_bacnet/__init__.py @@ -11,6 +11,7 @@ from .coordinator import FlexitCoordinator PLATFORMS: list[Platform] = [ Platform.BINARY_SENSOR, Platform.CLIMATE, + Platform.NUMBER, Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/flexit_bacnet/icons.json b/homeassistant/components/flexit_bacnet/icons.json new file mode 100644 index 00000000000..0e1305082ce --- /dev/null +++ b/homeassistant/components/flexit_bacnet/icons.json @@ -0,0 +1,36 @@ +{ + "entity": { + "number": { + "away_extract_fan_setpoint": { + "default": "mdi:fan-minus" + }, + "away_supply_fan_setpoint": { + "default": "mdi:fan-plus" + }, + "cooker_hood_extract_fan_setpoint": { + "default": "mdi:fan-minus" + }, + "cooker_hood_supply_fan_setpoint": { + "default": "mdi:fan-plus" + }, + "fireplace_extract_fan_setpoint": { + "default": "mdi:fan-minus" + }, + "fireplace_supply_fan_setpoint": { + "default": "mdi:fan-plus" + }, + "high_extract_fan_setpoint": { + "default": "mdi:fan-minus" + }, + "high_supply_fan_setpoint": { + "default": "mdi:fan-plus" + }, + "home_extract_fan_setpoint": { + "default": "mdi:fan-minus" + }, + "home_supply_fan_setpoint": { + "default": "mdi:fan-plus" + } + } + } +} diff --git a/homeassistant/components/flexit_bacnet/number.py b/homeassistant/components/flexit_bacnet/number.py new file mode 100644 index 00000000000..2731d5e8b09 --- /dev/null +++ b/homeassistant/components/flexit_bacnet/number.py @@ -0,0 +1,204 @@ +"""The Flexit Nordic (BACnet) integration.""" +import asyncio.exceptions +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from flexit_bacnet import FlexitBACnet +from flexit_bacnet.bacnet import DecodingError + +from homeassistant.components.number import ( + NumberDeviceClass, + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import FlexitCoordinator +from .const import DOMAIN +from .entity import FlexitEntity + + +@dataclass(kw_only=True, frozen=True) +class FlexitNumberEntityDescription(NumberEntityDescription): + """Describes a Flexit number entity.""" + + native_value_fn: Callable[[FlexitBACnet], float] + set_native_value_fn: Callable[[FlexitBACnet], Callable[[int], Awaitable[None]]] + + +NUMBERS: tuple[FlexitNumberEntityDescription, ...] = ( + FlexitNumberEntityDescription( + key="away_extract_fan_setpoint", + translation_key="away_extract_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_extract_air_away, + set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_away, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="away_supply_fan_setpoint", + translation_key="away_supply_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_supply_air_away, + set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_away, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="cooker_hood_extract_fan_setpoint", + translation_key="cooker_hood_extract_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_extract_air_cooker, + set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_cooker, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="cooker_hood_supply_fan_setpoint", + translation_key="cooker_hood_supply_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_supply_air_cooker, + set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_cooker, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="fireplace_extract_fan_setpoint", + translation_key="fireplace_extract_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_extract_air_fire, + set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_fire, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="fireplace_supply_fan_setpoint", + translation_key="fireplace_supply_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_supply_air_fire, + set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_fire, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="high_extract_fan_setpoint", + translation_key="high_extract_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_extract_air_high, + set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_high, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="high_supply_fan_setpoint", + translation_key="high_supply_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_supply_air_high, + set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_high, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="home_extract_fan_setpoint", + translation_key="home_extract_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_extract_air_home, + set_native_value_fn=lambda device: device.set_fan_setpoint_extract_air_home, + native_unit_of_measurement=PERCENTAGE, + ), + FlexitNumberEntityDescription( + key="home_supply_fan_setpoint", + translation_key="home_supply_fan_setpoint", + device_class=NumberDeviceClass.POWER_FACTOR, + native_min_value=0, + native_max_value=100, + native_step=1, + mode=NumberMode.SLIDER, + native_value_fn=lambda device: device.fan_setpoint_supply_air_home, + set_native_value_fn=lambda device: device.set_fan_setpoint_supply_air_home, + native_unit_of_measurement=PERCENTAGE, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Flexit (bacnet) number from a config entry.""" + coordinator: FlexitCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + FlexitNumber(coordinator, description) for description in NUMBERS + ) + + +class FlexitNumber(FlexitEntity, NumberEntity): + """Representation of a Flexit Number.""" + + entity_description: FlexitNumberEntityDescription + + def __init__( + self, + coordinator: FlexitCoordinator, + entity_description: FlexitNumberEntityDescription, + ) -> None: + """Initialize Flexit (bacnet) number.""" + super().__init__(coordinator) + + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.device.serial_number}-{entity_description.key}" + ) + + @property + def native_value(self) -> float: + """Return the state of the number.""" + return self.entity_description.native_value_fn(self.coordinator.device) + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + set_native_value_fn = self.entity_description.set_native_value_fn( + self.coordinator.device + ) + try: + await set_native_value_fn(int(value)) + except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc: + raise HomeAssistantError from exc + finally: + await self.coordinator.async_refresh() diff --git a/homeassistant/components/flexit_bacnet/strings.json b/homeassistant/components/flexit_bacnet/strings.json index d9efd1fc411..7f763674d00 100644 --- a/homeassistant/components/flexit_bacnet/strings.json +++ b/homeassistant/components/flexit_bacnet/strings.json @@ -22,6 +22,38 @@ "name": "Air filter polluted" } }, + "number": { + "away_extract_fan_setpoint": { + "name": "Away extract fan setpoint" + }, + "away_supply_fan_setpoint": { + "name": "Away supply fan setpoint" + }, + "cooker_hood_extract_fan_setpoint": { + "name": "Cooker hood extract fan setpoint" + }, + "cooker_hood_supply_fan_setpoint": { + "name": "Cooker hood supply fan setpoint" + }, + "fireplace_extract_fan_setpoint": { + "name": "Fireplace extract fan setpoint" + }, + "fireplace_supply_fan_setpoint": { + "name": "Fireplace supply fan setpoint" + }, + "high_extract_fan_setpoint": { + "name": "High extract fan setpoint" + }, + "high_supply_fan_setpoint": { + "name": "High supply fan setpoint" + }, + "home_extract_fan_setpoint": { + "name": "Home extract fan setpoint" + }, + "home_supply_fan_setpoint": { + "name": "Home supply fan setpoint" + } + }, "sensor": { "outside_air_temperature": { "name": "Outside air temperature" diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index c192489805f..307dbcfdd71 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -62,6 +62,18 @@ def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]: flexit_bacnet.air_filter_polluted = False flexit_bacnet.electric_heater = True + # Mock fan setpoints + flexit_bacnet.fan_setpoint_extract_air_fire = 10 + flexit_bacnet.fan_setpoint_supply_air_fire = 20 + flexit_bacnet.fan_setpoint_extract_air_away = 30 + flexit_bacnet.fan_setpoint_supply_air_away = 40 + flexit_bacnet.fan_setpoint_extract_air_home = 50 + flexit_bacnet.fan_setpoint_supply_air_home = 60 + flexit_bacnet.fan_setpoint_extract_air_high = 70 + flexit_bacnet.fan_setpoint_supply_air_high = 80 + flexit_bacnet.fan_setpoint_extract_air_cooker = 90 + flexit_bacnet.fan_setpoint_supply_air_cooker = 100 + yield flexit_bacnet diff --git a/tests/components/flexit_bacnet/snapshots/test_number.ambr b/tests/components/flexit_bacnet/snapshots/test_number.ambr new file mode 100644 index 00000000000..7808a62c6ac --- /dev/null +++ b/tests/components/flexit_bacnet/snapshots/test_number.ambr @@ -0,0 +1,541 @@ +# serializer version: 1 +# name: test_numbers[number.device_name_away_extract_fan_setpoint-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.device_name_away_extract_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Away extract fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'away_extract_fan_setpoint', + 'unique_id': '0000-0001-away_extract_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_away_extract_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Away extract fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_away_extract_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '30', + }) +# --- +# name: test_numbers[number.device_name_away_supply_fan_setpoint-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.device_name_away_supply_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Away supply fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'away_supply_fan_setpoint', + 'unique_id': '0000-0001-away_supply_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_away_supply_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Away supply fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_away_supply_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '40', + }) +# --- +# name: test_numbers[number.device_name_cooker_hood_extract_fan_setpoint-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.device_name_cooker_hood_extract_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cooker hood extract fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooker_hood_extract_fan_setpoint', + 'unique_id': '0000-0001-cooker_hood_extract_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_cooker_hood_extract_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Cooker hood extract fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_cooker_hood_extract_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '90', + }) +# --- +# name: test_numbers[number.device_name_cooker_hood_supply_fan_setpoint-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.device_name_cooker_hood_supply_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Cooker hood supply fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'cooker_hood_supply_fan_setpoint', + 'unique_id': '0000-0001-cooker_hood_supply_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_cooker_hood_supply_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Cooker hood supply fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_cooker_hood_supply_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '100', + }) +# --- +# name: test_numbers[number.device_name_fireplace_extract_fan_setpoint-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.device_name_fireplace_extract_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fireplace extract fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fireplace_extract_fan_setpoint', + 'unique_id': '0000-0001-fireplace_extract_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_fireplace_extract_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Fireplace extract fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_fireplace_extract_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '10', + }) +# --- +# name: test_numbers[number.device_name_fireplace_supply_fan_setpoint-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.device_name_fireplace_supply_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Fireplace supply fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'fireplace_supply_fan_setpoint', + 'unique_id': '0000-0001-fireplace_supply_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_fireplace_supply_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Fireplace supply fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_fireplace_supply_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '20', + }) +# --- +# name: test_numbers[number.device_name_high_extract_fan_setpoint-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.device_name_high_extract_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'High extract fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'high_extract_fan_setpoint', + 'unique_id': '0000-0001-high_extract_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_high_extract_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name High extract fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_high_extract_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '70', + }) +# --- +# name: test_numbers[number.device_name_high_supply_fan_setpoint-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.device_name_high_supply_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'High supply fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'high_supply_fan_setpoint', + 'unique_id': '0000-0001-high_supply_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_high_supply_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name High supply fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_high_supply_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '80', + }) +# --- +# name: test_numbers[number.device_name_home_extract_fan_setpoint-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.device_name_home_extract_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Home extract fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'home_extract_fan_setpoint', + 'unique_id': '0000-0001-home_extract_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_home_extract_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Home extract fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_home_extract_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '50', + }) +# --- +# name: test_numbers[number.device_name_home_supply_fan_setpoint-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.device_name_home_supply_fan_setpoint', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Home supply fan setpoint', + 'platform': 'flexit_bacnet', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'home_supply_fan_setpoint', + 'unique_id': '0000-0001-home_supply_fan_setpoint', + 'unit_of_measurement': '%', + }) +# --- +# name: test_numbers[number.device_name_home_supply_fan_setpoint-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power_factor', + 'friendly_name': 'Device Name Home supply fan setpoint', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 1, + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'number.device_name_home_supply_fan_setpoint', + 'last_changed': , + 'last_updated': , + 'state': '60', + }) +# --- diff --git a/tests/components/flexit_bacnet/test_number.py b/tests/components/flexit_bacnet/test_number.py new file mode 100644 index 00000000000..6f23ee11866 --- /dev/null +++ b/tests/components/flexit_bacnet/test_number.py @@ -0,0 +1,115 @@ +"""Tests for the Flexit Nordic (BACnet) number entities.""" +from unittest.mock import AsyncMock + +from flexit_bacnet import DecodingError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN +from homeassistant.components.number.const import ATTR_VALUE, SERVICE_SET_VALUE +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry +from tests.components.flexit_bacnet import setup_with_selected_platforms + +ENTITY_ID = "number.device_name_fireplace_supply_fan_setpoint" + + +async def test_numbers( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_flexit_bacnet: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test number states are correctly collected from library.""" + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + entity_entries = er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + + assert entity_entries + for entity_entry in entity_entries: + assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry") + assert (state := hass.states.get(entity_entry.entity_id)) + assert state == snapshot(name=f"{entity_entry.entity_id}-state") + + +async def test_numbers_implementation( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_flexit_bacnet: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that the number can be changed.""" + + await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER]) + + mock_flexit_bacnet.fan_setpoint_supply_air_fire = 60 + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 60, + }, + blocking=True, + ) + + mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + assert len(mocked_method.mock_calls) == 1 + assert hass.states.get(ENTITY_ID).state == "60" + + mock_flexit_bacnet.fan_setpoint_supply_air_fire = 10 + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + assert len(mocked_method.mock_calls) == 2 + assert hass.states.get(ENTITY_ID).state == "10" + + # Error recovery, when setting the value + mock_flexit_bacnet.set_fan_setpoint_supply_air_fire.side_effect = DecodingError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 10, + }, + blocking=True, + ) + + mocked_method = getattr(mock_flexit_bacnet, "set_fan_setpoint_supply_air_fire") + assert len(mocked_method.mock_calls) == 3 + + mock_flexit_bacnet.set_fan_setpoint_supply_air_fire.side_effect = None + mock_flexit_bacnet.fan_setpoint_supply_air_fire = 30 + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_VALUE: 30, + }, + blocking=True, + ) + + assert hass.states.get(ENTITY_ID).state == "30"