Skip to content

Commit 9c75c6f

Browse files
committed
feat: fix some issues and move to bluetooth update coordinator
1 parent 2c7342f commit 9c75c6f

File tree

7 files changed

+143
-107
lines changed

7 files changed

+143
-107
lines changed
Lines changed: 10 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
11
"""The ac_infinity integration."""
22
from __future__ import annotations
33

4-
import asyncio
5-
from datetime import timedelta
64
import logging
75

8-
import async_timeout
96
from ac_infinity_ble import ACInfinityController, DeviceInfo
107

118
from homeassistant.components import bluetooth
12-
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
139
from homeassistant.config_entries import ConfigEntry
1410
from homeassistant.const import (
1511
CONF_ADDRESS,
1612
CONF_SERVICE_DATA,
17-
EVENT_HOMEASSISTANT_STOP,
1813
Platform,
1914
)
20-
from homeassistant.core import Event, HomeAssistant, callback
15+
from homeassistant.core import HomeAssistant
2116
from homeassistant.exceptions import ConfigEntryNotReady
22-
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
2317

24-
from .const import BLEAK_EXCEPTIONS, DEVICE_TIMEOUT, DOMAIN, UPDATE_SECONDS
18+
from .const import DOMAIN
19+
from .coordinator import ACInfinityDataUpdateCoordinator
2520
from .models import ACInfinityData
2621

2722
PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.FAN]
@@ -42,78 +37,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
4237
if type(device_info) is dict:
4338
device_info = DeviceInfo(**entry.data[CONF_SERVICE_DATA])
4439
controller = ACInfinityController(ble_device, device_info)
45-
46-
@callback
47-
def _async_update_ble(
48-
service_info: bluetooth.BluetoothServiceInfoBleak,
49-
change: bluetooth.BluetoothChange,
50-
) -> None:
51-
"""Update from a ble callback."""
52-
controller.set_ble_device_and_advertisement_data(
53-
service_info.device, service_info.advertisement
54-
)
55-
56-
entry.async_on_unload(
57-
bluetooth.async_register_callback(
58-
hass,
59-
_async_update_ble,
60-
BluetoothCallbackMatcher({ADDRESS: address}),
61-
bluetooth.BluetoothScanningMode.PASSIVE,
62-
)
63-
)
64-
65-
async def _async_update():
66-
"""Update the device state."""
67-
try:
68-
await controller.update()
69-
await controller.stop()
70-
except BLEAK_EXCEPTIONS as ex:
71-
raise UpdateFailed(str(ex)) from ex
72-
73-
startup_event = asyncio.Event()
74-
cancel_first_update = controller.register_callback(lambda *_: startup_event.set())
75-
coordinator = DataUpdateCoordinator(
76-
hass,
77-
_LOGGER,
78-
name=controller.name,
79-
update_method=_async_update,
80-
update_interval=timedelta(seconds=UPDATE_SECONDS),
81-
)
82-
83-
try:
84-
await coordinator.async_config_entry_first_refresh()
85-
except ConfigEntryNotReady:
86-
cancel_first_update()
87-
raise
88-
89-
try:
90-
async with async_timeout.timeout(DEVICE_TIMEOUT):
91-
await startup_event.wait()
92-
except asyncio.TimeoutError as ex:
93-
raise ConfigEntryNotReady(
94-
"Unable to communicate with the device; "
95-
f"Try moving the Bluetooth adapter closer to {controller.name}"
96-
) from ex
97-
finally:
98-
cancel_first_update()
40+
coordinator = ACInfinityDataUpdateCoordinator(hass, _LOGGER, ble_device, controller)
9941

10042
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = ACInfinityData(
10143
entry.title, controller, coordinator
10244
)
10345

104-
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
105-
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
46+
entry.async_on_unload(coordinator.async_start())
47+
if not await coordinator.async_wait_ready():
48+
raise ConfigEntryNotReady(f"{address} is not advertising state")
10649

107-
async def _async_stop(event: Event) -> None:
108-
"""Close the connection."""
109-
try:
110-
await controller.stop()
111-
except BLEAK_EXCEPTIONS:
112-
pass
50+
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
51+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
11352

114-
entry.async_on_unload(
115-
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop)
116-
)
11753
return True
11854

11955

@@ -127,10 +63,6 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> Non
12763
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
12864
"""Unload a config entry."""
12965
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
130-
data: ACInfinityData = hass.data[DOMAIN].pop(entry.entry_id)
131-
try:
132-
await data.device.stop()
133-
except BLEAK_EXCEPTIONS:
134-
pass
66+
hass.data[DOMAIN].pop(entry.entry_id)
13567

13668
return unload_ok

custom_components/ac_infinity/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
DEVICE_TIMEOUT = 30
77
UPDATE_SECONDS = 15
88

9-
BLEAK_EXCEPTIONS = (AttributeError, BleakError)
9+
BLEAK_EXCEPTIONS = (AttributeError, BleakError, TimeoutError)
1010

1111
DEVICE_MODEL = {1: "Controller 67", 7: "Controller 69", 11: "Controller 69 Pro"}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""AC Infinity Coordinator."""
2+
from __future__ import annotations
3+
4+
import asyncio
5+
import contextlib
6+
import logging
7+
8+
from ac_infinity_ble import ACInfinityController
9+
import async_timeout
10+
from bleak.backends.device import BLEDevice
11+
12+
from homeassistant.components import bluetooth
13+
from homeassistant.components.bluetooth.active_update_coordinator import (
14+
ActiveBluetoothDataUpdateCoordinator,
15+
)
16+
from homeassistant.core import CoreState, HomeAssistant, callback
17+
18+
19+
DEVICE_STARTUP_TIMEOUT = 30
20+
21+
22+
class ACInfinityDataUpdateCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
23+
"""Class to manage fetching switchbot data."""
24+
25+
def __init__(
26+
self,
27+
hass: HomeAssistant,
28+
logger: logging.Logger,
29+
ble_device: BLEDevice,
30+
controller: ACInfinityController,
31+
) -> None:
32+
"""Initialize global switchbot data updater."""
33+
super().__init__(
34+
hass=hass,
35+
logger=logger,
36+
address=ble_device.address,
37+
needs_poll_method=self._needs_poll,
38+
poll_method=self._async_update,
39+
mode=bluetooth.BluetoothScanningMode.ACTIVE,
40+
connectable=True,
41+
)
42+
self.ble_device = ble_device
43+
self.controller = controller
44+
self._ready_event = asyncio.Event()
45+
self._was_unavailable = True
46+
47+
@callback
48+
def _needs_poll(
49+
self,
50+
service_info: bluetooth.BluetoothServiceInfoBleak,
51+
seconds_since_last_poll: float | None,
52+
) -> bool:
53+
# Only poll if hass is running, we need to poll,
54+
# and we actually have a way to connect to the device
55+
return (
56+
self.hass.state == CoreState.running
57+
and (seconds_since_last_poll is None or seconds_since_last_poll > 30)
58+
and bool(
59+
bluetooth.async_ble_device_from_address(
60+
self.hass, service_info.device.address, connectable=True
61+
)
62+
)
63+
)
64+
65+
async def _async_update(
66+
self, service_info: bluetooth.BluetoothServiceInfoBleak
67+
) -> None:
68+
"""Poll the device."""
69+
await self.controller.update()
70+
71+
@callback
72+
def _async_handle_unavailable(
73+
self, service_info: bluetooth.BluetoothServiceInfoBleak
74+
) -> None:
75+
"""Handle the device going unavailable."""
76+
super()._async_handle_unavailable(service_info)
77+
self._was_unavailable = True
78+
79+
@callback
80+
def _async_handle_bluetooth_event(
81+
self,
82+
service_info: bluetooth.BluetoothServiceInfoBleak,
83+
change: bluetooth.BluetoothChange,
84+
) -> None:
85+
"""Handle a Bluetooth event."""
86+
self.ble_device = service_info.device
87+
self.controller.set_ble_device_and_advertisement_data(
88+
service_info.device, service_info.advertisement
89+
)
90+
if self.controller.name:
91+
self._ready_event.set()
92+
self.logger.debug(
93+
"%s: AC Infinity data: %s", self.ble_device.address, self.controller.state
94+
)
95+
self._was_unavailable = False
96+
super()._async_handle_bluetooth_event(service_info, change)
97+
98+
async def async_wait_ready(self) -> bool:
99+
"""Wait for the device to be ready."""
100+
with contextlib.suppress(asyncio.TimeoutError):
101+
async with async_timeout.timeout(DEVICE_STARTUP_TIMEOUT):
102+
await self._ready_event.wait()
103+
return True
104+
return False

custom_components/ac_infinity/fan.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,22 @@
88

99
from homeassistant.components.fan import FanEntity, FanEntityFeature
1010

11+
from homeassistant.components.bluetooth.passive_update_coordinator import (
12+
PassiveBluetoothCoordinatorEntity,
13+
)
1114
from homeassistant.config_entries import ConfigEntry
1215
from homeassistant.core import HomeAssistant, callback
1316
from homeassistant.helpers import device_registry as dr
1417
from homeassistant.helpers.entity import DeviceInfo
1518
from homeassistant.helpers.entity_platform import AddEntitiesCallback
16-
from homeassistant.helpers.update_coordinator import (
17-
CoordinatorEntity,
18-
DataUpdateCoordinator,
19-
)
2019
from homeassistant.util.percentage import (
2120
int_states_in_range,
2221
ranged_value_to_percentage,
2322
percentage_to_ranged_value,
2423
)
2524

2625
from .const import DEVICE_MODEL, DOMAIN
26+
from .coordinator import ACInfinityDataUpdateCoordinator
2727
from .models import ACInfinityData
2828

2929
SPEED_RANGE = (1, 10)
@@ -39,15 +39,17 @@ async def async_setup_entry(
3939
async_add_entities([ACInfinityFan(data.coordinator, data.device, entry.title)])
4040

4141

42-
class ACInfinityFan(CoordinatorEntity, FanEntity):
42+
class ACInfinityFan(
43+
PassiveBluetoothCoordinatorEntity[ACInfinityDataUpdateCoordinator], FanEntity
44+
):
4345
"""Representation of AC Infinity sensor."""
4446

4547
_attr_speed_count = int_states_in_range(SPEED_RANGE)
4648
_attr_supported_features = FanEntityFeature.SET_SPEED
4749

4850
def __init__(
4951
self,
50-
coordinator: DataUpdateCoordinator,
52+
coordinator: ACInfinityDataUpdateCoordinator,
5153
device: ACInfinityController,
5254
name: str,
5355
) -> None:
@@ -72,7 +74,6 @@ async def async_set_percentage(self, percentage: int) -> None:
7274
speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
7375

7476
await self._device.set_speed(speed)
75-
self._async_update_attrs()
7677

7778
async def async_turn_on(
7879
self,
@@ -85,12 +86,10 @@ async def async_turn_on(
8586
if percentage is not None:
8687
speed = math.ceil(percentage_to_ranged_value(SPEED_RANGE, percentage))
8788
await self._device.turn_on(speed)
88-
self._async_update_attrs()
8989

9090
async def async_turn_off(self, **kwargs: Any) -> None:
9191
"""Turn off the fan."""
9292
await self._device.turn_off()
93-
self._async_update_attrs()
9493

9594
@callback
9695
def _async_update_attrs(self) -> None:

custom_components/ac_infinity/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@
1919
"issue_tracker": "https://github.com/hunterjm/ac-infinity-hacs/issues",
2020
"iot_class": "local_push",
2121
"requirements": [
22-
"ac-infinity-ble==0.4.1"
22+
"ac-infinity-ble==0.4.2"
2323
]
2424
}

custom_components/ac_infinity/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ac_infinity_ble import ACInfinityController
77

8-
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
8+
from .coordinator import ACInfinityDataUpdateCoordinator
99

1010

1111
@dataclass
@@ -14,4 +14,4 @@ class ACInfinityData:
1414

1515
title: str
1616
device: ACInfinityController
17-
coordinator: DataUpdateCoordinator
17+
coordinator: ACInfinityDataUpdateCoordinator

custom_components/ac_infinity/sensor.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,18 @@
1010
SensorStateClass,
1111
)
1212

13+
from homeassistant.components.bluetooth.passive_update_coordinator import (
14+
PassiveBluetoothCoordinatorEntity,
15+
)
1316
from homeassistant.config_entries import ConfigEntry
1417
from homeassistant.const import PERCENTAGE, UnitOfPressure, UnitOfTemperature
1518
from homeassistant.core import HomeAssistant, callback
1619
from homeassistant.helpers import device_registry as dr
1720
from homeassistant.helpers.entity import DeviceInfo
1821
from homeassistant.helpers.entity_platform import AddEntitiesCallback
19-
from homeassistant.helpers.typing import UndefinedType
20-
from homeassistant.helpers.update_coordinator import (
21-
CoordinatorEntity,
22-
DataUpdateCoordinator,
23-
)
2422

2523
from .const import DEVICE_MODEL, DOMAIN
24+
from .coordinator import ACInfinityDataUpdateCoordinator
2625
from .models import ACInfinityData
2726

2827

@@ -33,21 +32,23 @@ async def async_setup_entry(
3332
) -> None:
3433
"""Set up the light platform for LEDBLE."""
3534
data: ACInfinityData = hass.data[DOMAIN][entry.entry_id]
36-
async_add_entities(
37-
[
38-
TemperatureSensor(data.coordinator, data.device, entry.title),
39-
HumiditySensor(data.coordinator, data.device, entry.title),
40-
VpdSensor(data.coordinator, data.device, entry.title),
41-
]
42-
)
43-
44-
45-
class ACInfinitySensor(CoordinatorEntity, SensorEntity):
35+
entities = [
36+
TemperatureSensor(data.coordinator, data.device, entry.title),
37+
HumiditySensor(data.coordinator, data.device, entry.title),
38+
]
39+
if data.device.state.version >= 3 and data.device.state.type in [7, 9, 11, 12]:
40+
entities.append(VpdSensor(data.coordinator, data.device, entry.title))
41+
async_add_entities(entities)
42+
43+
44+
class ACInfinitySensor(
45+
PassiveBluetoothCoordinatorEntity[ACInfinityDataUpdateCoordinator], SensorEntity
46+
):
4647
"""Representation of AC Infinity sensor."""
4748

4849
def __init__(
4950
self,
50-
coordinator: DataUpdateCoordinator,
51+
coordinator: ACInfinityDataUpdateCoordinator,
5152
device: ACInfinityController,
5253
name: str,
5354
) -> None:

0 commit comments

Comments
 (0)