Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 62 additions & 22 deletions custom_components/ha_tion_btle/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
"""The Tion breezer component."""
from __future__ import annotations

from bleak.backends.device import BLEDevice
import asyncio
import datetime
import logging
import math
from datetime import timedelta
from collections.abc import Awaitable, Callable
from functools import cached_property

from bleak import BleakClient
from bleak.backends.device import BLEDevice
from bleak_retry_connector import BLEAK_RETRY_EXCEPTIONS, establish_connection
import tion_btle
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothCallbackMatcher
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from tion_btle.tion import Tion, MaxTriesExceededError
from tion_btle.tion import MaxTriesExceededError, Tion
from .const import DOMAIN, TION_SCHEMA, CONF_KEEP_ALIVE, CONF_AWAY_TEMP, CONF_MAC, PLATFORMS
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
Expand Down Expand Up @@ -64,10 +67,16 @@ def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry):
except KeyError:
pass

# delay before next update if we got btle.BTLEDisconnectError
self._delay: int = 600
# A short backoff keeps a transient BLE failure from making controls
# appear broken for ten minutes.
self._delay: int = 30
self._operation_lock = asyncio.Lock()

self.__tion: Tion = self.getTion(self.model, btle_device)
self.__tion: Tion = self.getTion(
self.model,
btle_device,
connection_factory=self._establish_connection,
)
self.__keep_alive = datetime.timedelta(seconds=self.__keep_alive)
self._delay = datetime.timedelta(seconds=self._delay)
self.rssi: int = 0
Expand Down Expand Up @@ -112,17 +121,23 @@ async def async_update_state(self):
response: dict[str, str | bool | int] = {}

try:
response = await self.__tion.get()
async with self._operation_lock:
self._refresh_btle_device()
response = await self.__tion.get()
self.update_interval = self.__keep_alive

except MaxTriesExceededError as e:
_LOGGER.critical("Got exception %s", str(e))
_LOGGER.critical("Will delay next check")
_LOGGER.warning("Could not connect to Tion: %s", str(e))
self.update_interval = self._delay
raise UpdateFailed("MaxTriesExceededError")
raise UpdateFailed("Could not connect to Tion") from e
except BLEAK_RETRY_EXCEPTIONS as e:
_LOGGER.warning("Bluetooth operation failed: %s", str(e))
self.update_interval = self._delay
raise UpdateFailed(f"Bluetooth operation failed: {e}") from e
except Exception as e:
_LOGGER.critical(f"{response=}, {e=}")
raise e
_LOGGER.warning("Could not update Tion state: %s", e, exc_info=True)
self.update_interval = self._delay
raise UpdateFailed(f"Could not update Tion state: {e}") from e

response["is_on"]: bool = self._decode_state(response["state"])
response["heater"]: bool = self._decode_state(response["heater"])
Expand Down Expand Up @@ -152,12 +167,19 @@ async def set(self, **kwargs):

args = ', '.join('%s=%r' % x for x in kwargs.items())
_LOGGER.info("Need to set: " + args)
await self.__tion.set(kwargs)
self.data.update(original_args)
self.async_update_listeners()
async with self._operation_lock:
self._refresh_btle_device()
await self.__tion.set(kwargs)
self.update_interval = self.__keep_alive
self.data.update(original_args)
self.async_update_listeners()

@staticmethod
def getTion(model: str, mac: str | BLEDevice) -> tion_btle.TionS3 | tion_btle.TionLite | tion_btle.TionS4:
def getTion(
model: str,
mac: str | BLEDevice,
connection_factory: Callable[[str | BLEDevice], Awaitable[BleakClient]] | None = None,
) -> tion_btle.TionS3 | tion_btle.TionLite | tion_btle.TionS4:
if model == 'S3':
from tion_btle.s3 import TionS3 as Breezer
elif model == 'S4':
Expand All @@ -166,13 +188,31 @@ def getTion(model: str, mac: str | BLEDevice) -> tion_btle.TionS3 | tion_btle.Ti
from tion_btle.lite import TionLite as Breezer
else:
raise NotImplementedError("Model '%s' is not supported!" % model)
return Breezer(mac)

async def connect(self):
return await self.__tion.connect()
return Breezer(mac, connection_factory=connection_factory)

async def _establish_connection(self, device: str | BLEDevice) -> BleakClient:
"""Establish a reliable connection using Home Assistant's BLE path."""
if not isinstance(device, BLEDevice):
raise ValueError("Home Assistant must provide a BLEDevice")
return await establish_connection(
BleakClient,
device,
self.config.get("name", self.config[CONF_MAC]),
max_attempts=3,
)

async def disconnect(self):
return await self.__tion.disconnect()
def _refresh_btle_device(self) -> None:
"""Select the best currently available adapter or Bluetooth proxy."""
device = bluetooth.async_ble_device_from_address(
self.hass,
self.config[CONF_MAC],
connectable=True,
)
if device is None:
raise UpdateFailed(
f"No connectable Bluetooth path to {self.config[CONF_MAC]}"
)
self.__tion.update_btle_device(device)

@property
def device_info(self):
Expand Down
21 changes: 6 additions & 15 deletions custom_components/ha_tion_btle/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,9 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode):

elif hvac_mode == HVACMode.HEAT:
saved_target_temp = self.target_temperature
try:
await self.coordinator.connect()
await self._async_set_state(heater=True, is_on=True)
if self.hvac_mode == HVACMode.FAN_ONLY:
await self.async_set_temperature(**{ATTR_TEMPERATURE: saved_target_temp})
finally:
await self.coordinator.disconnect()
await self._async_set_state(heater=True, is_on=True)
if self.hvac_mode == HVACMode.FAN_ONLY:
await self.async_set_temperature(**{ATTR_TEMPERATURE: saved_target_temp})
elif hvac_mode == HVACMode.FAN_ONLY:
await self._async_set_state(heater=False, is_on=True)

Expand Down Expand Up @@ -172,14 +168,9 @@ async def async_set_preset_mode(self, preset_mode: str):
self._saved_fan_mode = None

self._attr_preset_mode = preset_mode
try:
await self.coordinator.connect()
for a in actions:
await a[0](**a[1])
self._attr_preset_mode = preset_mode
self._handle_coordinator_update()
finally:
await self.coordinator.disconnect()
for a in actions:
await a[0](**a[1])
self._attr_preset_mode = preset_mode

self._handle_coordinator_update()

Expand Down
30 changes: 23 additions & 7 deletions custom_components/ha_tion_btle/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
import bleak
import tion_btle
import voluptuous as vol
from bleak import BleakClient
from bleak.backends.device import BLEDevice
from bleak_retry_connector import establish_connection

from homeassistant import config_entries
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import callback, async_get_hass
from homeassistant.core import callback
from tion_btle.tion import Tion

from .const import DOMAIN, TION_SCHEMA, CONF_MAC
Expand Down Expand Up @@ -93,15 +96,28 @@ def config(self) -> dict:
pass
return data

@staticmethod
def getTion(model: str, mac: str) -> tion_btle.TionS3 | tion_btle.TionLite | tion_btle.TionS4:
def getTion(self, model: str, mac: str) -> tion_btle.TionS3 | tion_btle.TionLite | tion_btle.TionS4:

btle_device = bluetooth.async_ble_device_from_address(hass=async_get_hass(), address=mac, connectable=True)
btle_device = bluetooth.async_ble_device_from_address(
hass=self.hass,
address=mac,
connectable=True,
)
if btle_device is None:
message = f"Could not find device with {mac=}"
_LOGGER.critical(f"getTion: {message}")
raise bleak.BleakError(message)

async def connection_factory(device: str | BLEDevice) -> BleakClient:
if not isinstance(device, BLEDevice):
raise ValueError("Home Assistant must provide a BLEDevice")
return await establish_connection(
BleakClient,
device,
mac,
max_attempts=3,
)

if model == 'S3':
from tion_btle.s3 import TionS3 as Breezer
elif model == 'S4':
Expand All @@ -110,13 +126,13 @@ def getTion(model: str, mac: str) -> tion_btle.TionS3 | tion_btle.TionLite | tio
from tion_btle.lite import TionLite as Breezer
else:
raise NotImplementedError("Model '%s' is not supported!" % model)
return Breezer(btle_device)
return Breezer(btle_device, connection_factory=connection_factory)


class TionConfigFlow(TionFlow, config_entries.ConfigFlow, domain=DOMAIN):
"""Initial setup."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
super().__init__()
Expand Down Expand Up @@ -146,7 +162,7 @@ async def async_step_user(self, input=None):
_LOGGER.debug(input)
try:
_tion: Tion = self.getTion(input['model'], input['mac'])
result = _tion.get()
result = await _tion.get()
except Exception as e:
_LOGGER.error("Could not get data from breezer. result is %s, error: %s" % (result, str(e)))
return self.async_show_form(step_id='add_failed')
Expand Down
5 changes: 3 additions & 2 deletions custom_components/ha_tion_btle/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
"documentation": "https://github.com/TionAPI/HA-tion/wiki",
"dependencies": [
"bluetooth",
"bluetooth_adapters",
"fan"
],
"requirements": [
"tion-btle==3.3.6"
"https://github.com/lifespirit/tion_python/archive/00b5527fc7194b66a58afcf939b244ed6d6dcff6.zip#tion-btle==3.3.7.dev0"
],
"codeowners": [
"@IATkachenko"
],
"config_flow": true,
"version": "v4.2.0"
"version": "v4.3.0-dev0"
}
3 changes: 2 additions & 1 deletion custom_components/ha_tion_btle/manifest.json.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
"documentation": "https://github.com/TionAPI/HA-tion/wiki",
"dependencies": [
"bluetooth",
"bluetooth_adapters",
"fan"
],
"requirements": [
"tion-btle==3.3.6"
"https://github.com/lifespirit/tion_python/archive/00b5527fc7194b66a58afcf939b244ed6d6dcff6.zip#tion-btle==3.3.7.dev0"
],
"codeowners": [
"@IATkachenko"
Expand Down