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
This commit is contained in:
Jonas Fors Lellky 2024-01-18 15:45:56 +01:00 committed by GitHub
parent c4f033e61c
commit bfe21b33f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 119 additions and 51 deletions

View file

@ -1,17 +1,12 @@
"""The Flexit Nordic (BACnet) integration.""" """The Flexit Nordic (BACnet) integration."""
from __future__ import annotations 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.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.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN from .const import DOMAIN
from .coordinator import FlexitCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE] PLATFORMS: list[Platform] = [Platform.CLIMATE]
@ -19,24 +14,19 @@ PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Flexit Nordic (BACnet) from a config entry.""" """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: coordinator = FlexitCoordinator(hass, device_id)
await device.update() await coordinator.async_config_entry_first_refresh()
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
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: 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): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)

View file

@ -6,7 +6,6 @@ from flexit_bacnet import (
VENTILATION_MODE_AWAY, VENTILATION_MODE_AWAY,
VENTILATION_MODE_HOME, VENTILATION_MODE_HOME,
VENTILATION_MODE_STOP, VENTILATION_MODE_STOP,
FlexitBACnet,
) )
from flexit_bacnet.bacnet import DecodingError 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.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
@ -32,6 +30,8 @@ from .const import (
PRESET_TO_VENTILATION_MODE_MAP, PRESET_TO_VENTILATION_MODE_MAP,
VENTILATION_TO_PRESET_MODE_MAP, VENTILATION_TO_PRESET_MODE_MAP,
) )
from .coordinator import FlexitCoordinator
from .entity import FlexitEntity
async def async_setup_entry( async def async_setup_entry(
@ -40,18 +40,16 @@ async def async_setup_entry(
async_add_devices: AddEntitiesCallback, async_add_devices: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the Flexit Nordic unit.""" """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.""" """Flexit air handling unit."""
_attr_name = None _attr_name = None
_attr_has_entity_name = True
_attr_hvac_modes = [ _attr_hvac_modes = [
HVACMode.OFF, HVACMode.OFF,
HVACMode.FAN_ONLY, HVACMode.FAN_ONLY,
@ -72,36 +70,27 @@ class FlexitClimateEntity(ClimateEntity):
_attr_max_temp = MAX_TEMP _attr_max_temp = MAX_TEMP
_attr_min_temp = MIN_TEMP _attr_min_temp = MIN_TEMP
def __init__(self, device: FlexitBACnet) -> None: def __init__(self, coordinator: FlexitCoordinator) -> None:
"""Initialize the unit.""" """Initialize the Flexit unit."""
self._device = device super().__init__(coordinator)
self._attr_unique_id = device.serial_number self._attr_unique_id = coordinator.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,
)
async def async_update(self) -> None: async def async_update(self) -> None:
"""Refresh unit state.""" """Refresh unit state."""
await self._device.update() await self.device.update()
@property @property
def current_temperature(self) -> float: def current_temperature(self) -> float:
"""Return the current temperature.""" """Return the current temperature."""
return self._device.room_temperature return self.device.room_temperature
@property @property
def target_temperature(self) -> float: def target_temperature(self) -> float:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._device.ventilation_mode == VENTILATION_MODE_AWAY: if self.device.ventilation_mode == VENTILATION_MODE_AWAY:
return self._device.air_temp_setpoint_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: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
@ -109,12 +98,14 @@ class FlexitClimateEntity(ClimateEntity):
return return
try: try:
if self._device.ventilation_mode == VENTILATION_MODE_AWAY: if self.device.ventilation_mode == VENTILATION_MODE_AWAY:
await self._device.set_air_temp_setpoint_away(temperature) await self.device.set_air_temp_setpoint_away(temperature)
else: 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: except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError from exc raise HomeAssistantError from exc
finally:
await self.coordinator.async_refresh()
@property @property
def preset_mode(self) -> str: def preset_mode(self) -> str:
@ -122,21 +113,23 @@ class FlexitClimateEntity(ClimateEntity):
Requires ClimateEntityFeature.PRESET_MODE. 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: async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode.""" """Set new preset mode."""
ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode] ventilation_mode = PRESET_TO_VENTILATION_MODE_MAP[preset_mode]
try: 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: except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError from exc raise HomeAssistantError from exc
finally:
await self.coordinator.async_refresh()
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode.""" """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.OFF
return HVACMode.FAN_ONLY return HVACMode.FAN_ONLY
@ -145,8 +138,10 @@ class FlexitClimateEntity(ClimateEntity):
"""Set new target hvac mode.""" """Set new target hvac mode."""
try: try:
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
await self._device.set_ventilation_mode(VENTILATION_MODE_STOP) await self.device.set_ventilation_mode(VENTILATION_MODE_STOP)
else: 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: except (asyncio.exceptions.TimeoutError, ConnectionError, DecodingError) as exc:
raise HomeAssistantError from exc raise HomeAssistantError from exc
finally:
await self.coordinator.async_refresh()

View file

@ -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

View file

@ -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

View file

@ -35,7 +35,7 @@ def mock_flexit_bacnet() -> Generator[AsyncMock, None, None]:
"homeassistant.components.flexit_bacnet.config_flow.FlexitBACnet", "homeassistant.components.flexit_bacnet.config_flow.FlexitBACnet",
return_value=flexit_bacnet, return_value=flexit_bacnet,
), patch( ), patch(
"homeassistant.components.flexit_bacnet.FlexitBACnet", "homeassistant.components.flexit_bacnet.coordinator.FlexitBACnet",
return_value=flexit_bacnet, return_value=flexit_bacnet,
): ):
flexit_bacnet.serial_number = "0000-0001" flexit_bacnet.serial_number = "0000-0001"