Skip to content

Commit 31d9001

Browse files
committed
Move door commands into RunChickenDevice; drop RunChickenController
RunChickenController held its own BleakClient snapshot, which went stale after a reconnect and forced the cover entity to repoint it before every command. Fold open/close into RunChickenDevice, which already owns the connection, so there is a single connection+command+notification owner and the stale-client class of bug is gone by construction. - parser.py: add async_open/async_close (retry-wrapped) writing via the live client, plus a shared _require_client helper. Remove the now-unused public `client` property. - cover.py: commands call device.async_open/async_close directly; the entity no longer creates or refreshes a controller, so its __init__ is just identity/device-info. - config_flow.py: the connectivity probe tears down via device.async_disconnect() instead of poking device.client. - Delete run_chicken_ble/cover.py and drop RunChickenController from the package API.
1 parent 11b1604 commit 31d9001

5 files changed

Lines changed: 32 additions & 122 deletions

File tree

custom_components/run_chicken/config_flow.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,5 @@ async def _async_try_connect(self, address: str) -> str | None:
122122
_LOGGER.exception("Unexpected error connecting to Run-Chicken device %s", address)
123123
return "unknown"
124124
finally:
125-
client = device.client
126-
if client is not None:
127-
await client.disconnect()
125+
await device.async_disconnect()
128126
return None

custom_components/run_chicken/cover.py

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,13 @@
1010
CoverEntity,
1111
CoverEntityDescription,
1212
)
13-
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
1413
from homeassistant.helpers.device_registry import (
1514
CONNECTION_BLUETOOTH,
1615
DeviceInfo,
1716
)
1817
from homeassistant.helpers.update_coordinator import CoordinatorEntity
1918

2019
from .coordinator import RunChickenCoordinator
21-
from .run_chicken_ble.cover import RunChickenController
2220
from .run_chicken_ble.models import RunChickenDoorState
2321

2422
_LOGGER = logging.getLogger(__name__)
@@ -57,12 +55,6 @@ def __init__(self, coordinator: RunChickenCoordinator) -> None:
5755
super().__init__(coordinator)
5856
self.run_chicken_device = coordinator.device
5957

60-
client = self.run_chicken_device.client
61-
if client is None:
62-
msg = "Run-Chicken device is not connected; cannot set up cover."
63-
raise ConfigEntryNotReady(msg)
64-
self.controller = RunChickenController(client=client)
65-
6658
self._attr_unique_id = f"run_chicken_{self.run_chicken_device.address}"
6759

6860
self._attr_device_info = DeviceInfo(
@@ -84,31 +76,13 @@ def is_closed(self) -> bool | None:
8476
return None
8577
return self.coordinator.data.door_state is RunChickenDoorState.CLOSED
8678

87-
async def _async_ready_controller(self) -> RunChickenController:
88-
"""
89-
Reconnect if needed and point the controller at the current client.
90-
91-
After a disconnect the device builds a fresh client, so the controller's
92-
captured client goes stale and writes fail with "characteristic not
93-
found". Refreshing it here keeps commands aligned with the live client.
94-
"""
95-
await self.run_chicken_device.ensure_client_connected()
96-
client = self.run_chicken_device.client
97-
if client is None:
98-
msg = "Run-Chicken device is not connected."
99-
raise HomeAssistantError(msg)
100-
self.controller.client = client
101-
return self.controller
102-
10379
async def async_open_cover(self, **kwargs: Any) -> None: # noqa: ARG002
104-
"""Open the coop door, reconnecting first if the connection dropped."""
105-
controller = await self._async_ready_controller()
106-
await controller.open_cover()
80+
"""Open the coop door (the device reconnects first if needed)."""
81+
await self.run_chicken_device.async_open()
10782

10883
async def async_close_cover(self, **kwargs: Any) -> None: # noqa: ARG002
109-
"""Close the coop door, reconnecting first if the connection dropped."""
110-
controller = await self._async_ready_controller()
111-
await controller.close_cover()
84+
"""Close the coop door (the device reconnects first if needed)."""
85+
await self.run_chicken_device.async_close()
11286

11387
def _handle_coordinator_update(self) -> None:
11488
"""Handle data update."""

custom_components/run_chicken/run_chicken_ble/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@
1212
defines the public surface of the package.
1313
"""
1414

15-
from .cover import RunChickenController
1615
from .models import RunChickenDeviceData
1716
from .parser import RunChickenDevice
1817

1918
__all__ = [
20-
"RunChickenController",
2119
"RunChickenDevice",
2220
"RunChickenDeviceData",
2321
]

custom_components/run_chicken/run_chicken_ble/cover.py

Lines changed: 0 additions & 77 deletions
This file was deleted.

custom_components/run_chicken/run_chicken_ble/parser.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
)
1414
from homeassistant.helpers.update_coordinator import UpdateFailed
1515

16-
from .const import READ_CHAR_UUID
16+
from .const import READ_CHAR_UUID, WRITE_CHAR_UUID
17+
from .create_packet import create_close_packet, create_open_packet
1718
from .models import RunChickenDeviceData, RunChickenDoorState
1819

1920
if TYPE_CHECKING:
@@ -52,11 +53,6 @@ def __init__(
5253
else:
5354
self._device_data: RunChickenDeviceData = device_data
5455

55-
@property
56-
def client(self) -> BleakClient | None:
57-
"""Return the BLE client."""
58-
return self._client
59-
6056
@property
6157
def address(self) -> str:
6258
"""Return the BLE address of the device."""
@@ -67,16 +63,21 @@ def device_data(self) -> RunChickenDeviceData:
6763
"""Return the device data."""
6864
return self._device_data
6965

70-
def _connected_read_char(self) -> tuple[BleakClient, BleakGATTCharacteristic]:
71-
"""Return the connected client and its read characteristic, or raise."""
66+
def _require_client(self) -> BleakClient:
67+
"""Return the connected client, or raise if not connected."""
7268
if self._client is None:
7369
msg = "Run-Chicken device is not connected."
7470
raise UpdateFailed(msg)
75-
char = self._client.services.get_characteristic(READ_CHAR_UUID)
71+
return self._client
72+
73+
def _connected_read_char(self) -> tuple[BleakClient, BleakGATTCharacteristic]:
74+
"""Return the connected client and its read characteristic, or raise."""
75+
client = self._require_client()
76+
char = client.services.get_characteristic(READ_CHAR_UUID)
7677
if char is None:
7778
msg = f"Read characteristic {READ_CHAR_UUID} not found on device."
7879
raise UpdateFailed(msg)
79-
return self._client, char
80+
return client, char
8081

8182
def set_disconnect_callback(self, callback: Callable[[], None]) -> None:
8283
"""
@@ -184,3 +185,19 @@ async def update_device(self) -> RunChickenDeviceData:
184185
await self.ensure_client_connected()
185186
self._device_data.door_state = self._parse_door_state(await self._read_payload())
186187
return self._device_data
188+
189+
@retry_bluetooth_connection_error()
190+
async def async_open(self) -> None:
191+
"""Open the Run-Chicken door."""
192+
await self._async_send_command(create_open_packet())
193+
194+
@retry_bluetooth_connection_error()
195+
async def async_close(self) -> None:
196+
"""Close the Run-Chicken door."""
197+
await self._async_send_command(create_close_packet())
198+
199+
async def _async_send_command(self, packet: bytes) -> None:
200+
"""Connect if needed and write a command packet to the door."""
201+
await self.ensure_client_connected()
202+
client = self._require_client()
203+
await client.write_gatt_char(WRITE_CHAR_UUID, packet)

0 commit comments

Comments
 (0)