From 077883d169c526e1f9754f14027dc3071091b9f6 Mon Sep 17 00:00:00 2001 From: Lennard Beers Date: Mon, 22 Jan 2024 23:02:58 +0100 Subject: [PATCH] feat: add connection monitoring and reconnect logic --- custom_components/eq3btsmart/binary_sensor.py | 29 +++++++++++++++++++ custom_components/eq3btsmart/const.py | 1 + .../eq3btsmart/eq3_coordinator.py | 16 ---------- eq3btsmart/eq3_connection_monitor.py | 24 +++++++++++++++ eq3btsmart/thermostat.py | 6 ++++ 5 files changed, 60 insertions(+), 16 deletions(-) delete mode 100644 custom_components/eq3btsmart/eq3_coordinator.py create mode 100644 eq3btsmart/eq3_connection_monitor.py diff --git a/custom_components/eq3btsmart/binary_sensor.py b/custom_components/eq3btsmart/binary_sensor.py index c6c2a03..a67366b 100644 --- a/custom_components/eq3btsmart/binary_sensor.py +++ b/custom_components/eq3btsmart/binary_sensor.py @@ -20,6 +20,7 @@ ENTITY_NAME_BUSY, ENTITY_NAME_CONNECTED, ENTITY_NAME_DST, + ENTITY_NAME_MONITORING, ENTITY_NAME_WINDOW_OPEN, ) @@ -45,6 +46,7 @@ async def async_setup_entry( entities_to_add += [ BusySensor(eq3_config, thermostat), ConnectedSensor(eq3_config, thermostat), + MonitoringSensor(eq3_config, thermostat), ] async_add_entities(entities_to_add) @@ -114,6 +116,33 @@ def is_on(self) -> bool: return self._thermostat._conn.is_connected +class MonitoringSensor(Base): + """Binary sensor that reports if the thermostat connection monitor is running.""" + + def __init__(self, eq3_config: Eq3Config, thermostat: Thermostat): + super().__init__(eq3_config, thermostat) + + self._thermostat.register_connection_callback(self.schedule_update_ha_state) + self._attr_entity_category = EntityCategory.DIAGNOSTIC + self._attr_name = ENTITY_NAME_MONITORING + self._attr_device_class = BinarySensorDeviceClass.RUNNING + + # @property + # def extra_state_attributes(self) -> dict[str, str] | None: + # if (device := self._thermostat._conn._device) is None: + # return None + # if (details := device.details) is None: + # return None + # if "props" not in details: + # return None + + # return json.loads(json.dumps(details["props"], default=lambda obj: None)) + + @property + def is_on(self) -> bool: + return self._thermostat._monitor._run + + class BatterySensor(Base): """Binary sensor that reports if the thermostat battery is low.""" diff --git a/custom_components/eq3btsmart/const.py b/custom_components/eq3btsmart/const.py index 4956b71..1b50e01 100644 --- a/custom_components/eq3btsmart/const.py +++ b/custom_components/eq3btsmart/const.py @@ -77,6 +77,7 @@ class Preset(str, Enum): ENTITY_NAME_AWAY_SWITCH = "Away" ENTITY_NAME_BOOST_SWITCH = "Boost" ENTITY_NAME_CONNECTION = "Connection" +ENTITY_NAME_MONITORING = "Monitoring" ENTITY_ICON_VALVE = "mdi:pipe-valve" ENTITY_ICON_AWAY_SWITCH = "mdi:lock" diff --git a/custom_components/eq3btsmart/eq3_coordinator.py b/custom_components/eq3btsmart/eq3_coordinator.py deleted file mode 100644 index 4f678c4..0000000 --- a/custom_components/eq3btsmart/eq3_coordinator.py +++ /dev/null @@ -1,16 +0,0 @@ -from datetime import timedelta -from logging import Logger - -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - - -class Eq3Coordinator(DataUpdateCoordinator): - def __init__( - self, - hass: HomeAssistant, - logger: Logger, - name: str, - update_interval: timedelta | None, - ): - super().__init__(hass, logger, name=name, update_interval=update_interval) diff --git a/eq3btsmart/eq3_connection_monitor.py b/eq3btsmart/eq3_connection_monitor.py new file mode 100644 index 0000000..204b262 --- /dev/null +++ b/eq3btsmart/eq3_connection_monitor.py @@ -0,0 +1,24 @@ +import asyncio + +from bleak import BleakClient + + +class Eq3ConnectionMonitor: + def __init__(self, client: BleakClient): + self._client = client + self._run = False + + async def run(self): + self._run = True + + while self._run: + try: + if not self._client.is_connected: + await self._client.connect() + except Exception: + pass + + await asyncio.sleep(5) + + async def stop(self): + self._run = False diff --git a/eq3btsmart/thermostat.py b/eq3btsmart/thermostat.py index cba95ac..a6ad9a3 100755 --- a/eq3btsmart/thermostat.py +++ b/eq3btsmart/thermostat.py @@ -33,6 +33,7 @@ WeekDay, ) from eq3btsmart.eq3_away_time import Eq3AwayTime +from eq3btsmart.eq3_connection_monitor import Eq3ConnectionMonitor from eq3btsmart.eq3_duration import Eq3Duration from eq3btsmart.eq3_temperature import Eq3Temperature from eq3btsmart.eq3_temperature_offset import Eq3TemperatureOffset @@ -84,6 +85,7 @@ def __init__( timeout=REQUEST_TIMEOUT, ) self._lock = asyncio.Lock() + self._monitor = Eq3ConnectionMonitor(self._conn) def register_connection_callback(self, on_connect: Callable) -> None: """Register a callback function that will be called when a connection is established.""" @@ -101,9 +103,13 @@ async def async_connect(self) -> None: await self._conn.connect() await self._conn.start_notify(PROP_NOTIFY_UUID, self.on_notification) + loop = asyncio.get_running_loop() + loop.create_task(self._monitor.run()) + async def async_disconnect(self) -> None: """Shutdown the connection to the thermostat.""" + await self._monitor.stop() await self._conn.disconnect() async def async_get_id(self) -> None: