From bfe21b33f063fa97edec6d06b632d58bcf1b326c Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Thu, 18 Jan 2024 15:45:56 +0100 Subject: [PATCH] Add coordinator to Flexit bacnet (#108295) * Adds coordinator and base entity class * Patch the coordinator * Adds device property to base class And refactors accordingly * Use const instead of string * Moves _attr_has_entity_name to base entity * Argument as positional * Use device_id from init --- .../components/flexit_bacnet/__init__.py | 24 +++----- .../components/flexit_bacnet/climate.py | 61 +++++++++---------- .../components/flexit_bacnet/coordinator.py | 49 +++++++++++++++ .../components/flexit_bacnet/entity.py | 34 +++++++++++ tests/components/flexit_bacnet/conftest.py | 2 +- 5 files changed, 119 insertions(+), 51 deletions(-) create mode 100644 homeassistant/components/flexit_bacnet/coordinator.py create mode 100644 homeassistant/components/flexit_bacnet/entity.py diff --git a/homeassistant/components/flexit_bacnet/__init__.py b/homeassistant/components/flexit_bacnet/__init__.py index 53ae1bbe775..39e06156a59 100644 --- a/homeassistant/components/flexit_bacnet/__init__.py +++ b/homeassistant/components/flexit_bacnet/__init__.py @@ -1,17 +1,12 @@ """The Flexit Nordic (BACnet) integration.""" from __future__ import annotations -import asyncio.exceptions - -from flexit_bacnet import FlexitBACnet -from flexit_bacnet.bacnet import DecodingError - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_DEVICE_ID, CONF_IP_ADDRESS, Platform +from homeassistant.const import CONF_DEVICE_ID, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from .const import DOMAIN +from .coordinator import FlexitCoordinator PLATFORMS: list[Platform] = [Platform.CLIMATE] @@ -19,24 +14,19 @@ PLATFORMS: list[Platform] = [Platform.CLIMATE] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Flexit Nordic (BACnet) from a config entry.""" - device = FlexitBACnet(entry.data[CONF_IP_ADDRESS], entry.data[CONF_DEVICE_ID]) + device_id = entry.data[CONF_DEVICE_ID] - try: - await device.update() - except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc: - raise ConfigEntryNotReady( - f"Timeout while connecting to {entry.data[CONF_IP_ADDRESS]}" - ) from exc - - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = device + coordinator = FlexitCoordinator(hass, device_id) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload a config entry.""" + """Unload the Flexit Nordic (BACnet) config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/flexit_bacnet/climate.py b/homeassistant/components/flexit_bacnet/climate.py index 79846bee019..7740bed73e1 100644 --- a/homeassistant/components/flexit_bacnet/climate.py +++ b/homeassistant/components/flexit_bacnet/climate.py @@ -6,7 +6,6 @@ from flexit_bacnet import ( VENTILATION_MODE_AWAY, VENTILATION_MODE_HOME, VENTILATION_MODE_STOP, - FlexitBACnet, ) from flexit_bacnet.bacnet import DecodingError @@ -22,7 +21,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -32,6 +30,8 @@ from .const import ( PRESET_TO_VENTILATION_MODE_MAP, VENTILATION_TO_PRESET_MODE_MAP, ) +from .coordinator import FlexitCoordinator +from .entity import FlexitEntity async def async_setup_entry( @@ -40,18 +40,16 @@ async def async_setup_entry( async_add_devices: AddEntitiesCallback, ) -> None: """Set up the Flexit Nordic unit.""" - device = hass.data[DOMAIN][config_entry.entry_id] + coordinator: FlexitCoordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_devices([FlexitClimateEntity(device)]) + async_add_devices([FlexitClimateEntity(coordinator)]) -class FlexitClimateEntity(ClimateEntity): +class FlexitClimateEntity(FlexitEntity, ClimateEntity): """Flexit air handling unit.""" _attr_name = None - _attr_has_entity_name = True - _attr_hvac_modes = [ HVACMode.OFF, HVACMode.FAN_ONLY, @@ -72,36 +70,27 @@ class FlexitClimateEntity(ClimateEntity): _attr_max_temp = MAX_TEMP _attr_min_temp = MIN_TEMP - def __init__(self, device: FlexitBACnet) -> None: - """Initialize the unit.""" - self._device = device - self._attr_unique_id = device.serial_number - self._attr_device_info = DeviceInfo( - identifiers={ - (DOMAIN, device.serial_number), - }, - name=device.device_name, - manufacturer="Flexit", - model="Nordic", - serial_number=device.serial_number, - ) + def __init__(self, coordinator: FlexitCoordinator) -> None: + """Initialize the Flexit unit.""" + super().__init__(coordinator) + self._attr_unique_id = coordinator.device.serial_number async def async_update(self) -> None: """Refresh unit state.""" - await self._device.update() + await self.device.update() @property def current_temperature(self) -> float: """Return the current temperature.""" - return self._device.room_temperature + return self.device.room_temperature @property def target_temperature(self) -> float: """Return the temperature we try to reach.""" - if self._device.ventilation_mode == VENTILATION_MODE_AWAY: - return self._device.air_temp_setpoint_away + if self.device.ventilation_mode == VENTILATION_MODE_AWAY: + return self.device.air_temp_setpoint_away - return self._device.air_temp_setpoint_home + return self.device.air_temp_setpoint_home async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" @@ -109,12 +98,14 @@ class FlexitClimateEntity(ClimateEntity): return try: - if self._device.ventilation_mode == VENTILATION_MODE_AWAY: - await self._device.set_air_temp_setpoint_away(temperature) + if self.device.ventilation_mode == VENTILATION_MODE_AWAY: + await self.device.set_air_temp_setpoint_away(temperature) else: - await self._device.set_air_temp_setpoint_home(temperature) + await self.device.set_air_temp_setpoint_home(temperature) except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc: raise HomeAssistantError from exc + finally: + await self.coordinator.async_refresh() @property def preset_mode(self) -> str: @@ -122,21 +113,23 @@ class FlexitClimateEntity(ClimateEntity): Requires ClimateEntityFeature.PRESET_MODE. """ - return VENTILATION_TO_PRESET_MODE_MAP[self._device.ventilation_mode] + return VENTILATION_TO_PRESET_MODE_MAP[self.device.ventilation_mode] async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode] try: - await self._device.set_ventilation_mode(ventilation_mode) + await self.device.set_ventilation_mode(ventilation_mode) except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc: raise HomeAssistantError from exc + finally: + await self.coordinator.async_refresh() @property def hvac_mode(self) -> HVACMode: """Return hvac operation ie. heat, cool mode.""" - if self._device.ventilation_mode == VENTILATION_MODE_STOP: + if self.device.ventilation_mode == VENTILATION_MODE_STOP: return HVACMode.OFF return HVACMode.FAN_ONLY @@ -145,8 +138,10 @@ class FlexitClimateEntity(ClimateEntity): """Set new target hvac mode.""" try: if hvac_mode == HVACMode.OFF: - await self._device.set_ventilation_mode(VENTILATION_MODE_STOP) + await self.device.set_ventilation_mode(VENTILATION_MODE_STOP) else: - await self._device.set_ventilation_mode(VENTILATION_MODE_HOME) + await self.device.set_ventilation_mode(VENTILATION_MODE_HOME) 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/coordinator.py b/homeassistant/components/flexit_bacnet/coordinator.py new file mode 100644 index 00000000000..556264e1268 --- /dev/null +++ b/homeassistant/components/flexit_bacnet/coordinator.py @@ -0,0 +1,49 @@ +"""DataUpdateCoordinator for Flexit Nordic (BACnet) integration..""" +import asyncio.exceptions +from datetime import timedelta +import logging + +from flexit_bacnet import FlexitBACnet +from flexit_bacnet.bacnet import DecodingError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID, CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class FlexitCoordinator(DataUpdateCoordinator[FlexitBACnet]): + """Class to manage fetching data from a Flexit Nordic (BACnet) device.""" + + config_entry: ConfigEntry + + def __init__(self, hass: HomeAssistant, device_id: str) -> None: + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_{device_id}", + update_interval=timedelta(seconds=60), + ) + + self.device = FlexitBACnet( + self.config_entry.data[CONF_IP_ADDRESS], + self.config_entry.data[CONF_DEVICE_ID], + ) + + async def _async_update_data(self) -> FlexitBACnet: + """Fetch data from the device.""" + + try: + await self.device.update() + except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc: + raise ConfigEntryNotReady( + f"Timeout while connecting to {self.config_entry.data[CONF_IP_ADDRESS]}" + ) from exc + + return self.device diff --git a/homeassistant/components/flexit_bacnet/entity.py b/homeassistant/components/flexit_bacnet/entity.py new file mode 100644 index 00000000000..3e00fae54af --- /dev/null +++ b/homeassistant/components/flexit_bacnet/entity.py @@ -0,0 +1,34 @@ +"""Base entity for the Flexit Nordic (BACnet) integration.""" +from __future__ import annotations + +from flexit_bacnet import FlexitBACnet + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import FlexitCoordinator + + +class FlexitEntity(CoordinatorEntity[FlexitCoordinator]): + """Defines a Flexit entity.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: FlexitCoordinator) -> None: + """Initialize a Flexit Nordic (BACnet) entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={ + (DOMAIN, coordinator.device.serial_number), + }, + name=coordinator.device.device_name, + manufacturer="Flexit", + model="Nordic", + serial_number=coordinator.device.serial_number, + ) + + @property + def device(self) -> FlexitBACnet: + """Return the device.""" + return self.coordinator.data diff --git a/tests/components/flexit_bacnet/conftest.py b/tests/components/flexit_bacnet/conftest.py index faa0bc6b7c0..0c6153e81c0 100644 --- a/tests/components/flexit_bacnet/conftest.py +++ b/tests/components/flexit_bacnet/conftest.py @@ -35,7 +35,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]: "homeassistant.components.flexit_bacnet.config_flow.FlexitBACnet", return_value=flexit_bacnet, ), patch( - "homeassistant.components.flexit_bacnet.FlexitBACnet", + "homeassistant.components.flexit_bacnet.coordinator.FlexitBACnet", return_value=flexit_bacnet, ): flexit_bacnet.serial_number = "0000-0001"