From 5138d8a37e7c3847bd9a8c4a2978ce9fdb6f69c7 Mon Sep 17 00:00:00 2001 From: Wesley Ray Date: Thu, 2 Apr 2026 16:04:20 -0400 Subject: [PATCH] Fix bleak 3.x compatibility and code review cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix scan_for_onecontrol() to use return_adv=True and adv.service_uuids (device.metadata removed in bleak 3.x) - Add missing _parse_get_devices_response() — was called but never defined, would crash on first real device response - main() now auto-connects when exactly one device is found via scan - Remove premature turn_on/off_light(5) calls from main() — device IDs unknown until get_devices() has been run against real hardware - Guard send_command() and disconnect() against unconnected state - control_awning() now takes MovementState enum instead of raw int - Bump post-get_devices sleep from 2s to 5s for real device latency Co-Authored-By: Claude Sonnet 4.6 --- src/onecontrol_client.py | 116 ++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 51 deletions(-) diff --git a/src/onecontrol_client.py b/src/onecontrol_client.py index ab4ce61..f081ef5 100644 --- a/src/onecontrol_client.py +++ b/src/onecontrol_client.py @@ -41,35 +41,34 @@ class OneControlClient: self.encoder = CobsEncoder() self.decoder = CobsDecoder() self._seq = 0 - self._pending_commands = {} # seq -> cmd_type + self._pending_commands = {} # seq -> cmd_type async def connect(self): """Connect to OneControl device""" self.client = BleakClient(self.address) await self.client.connect() - # Enable notifications await self.client.start_notify(self.READ_CHAR, self._notification_handler) async def disconnect(self): """Disconnect from device""" - if self.client: + if self.client and self.client.is_connected: await self.client.disconnect() def _notification_handler(self, sender, data): """Handle notifications from device""" try: decoded = self.decoder.decode(data) - # print(f"Received: {decoded.hex()}") - + # print(f"Received raw: {decoded.hex()}") + # Response Structure: [Event(1)] [Seq(2)] [RespType(1)] [Payload...] if len(decoded) < 4: return - + event_type = decoded[0] seq = struct.unpack("= 128: self._pending_commands.pop(seq, None) @@ -93,6 +91,23 @@ class OneControlClient: except Exception as e: print(f"Error decoding: {e}") + def _parse_get_devices_response(self, payload: bytes): + """Parse GetDevices response payload and print each device entry""" + if not payload: + print("GetDevices: empty payload") + return + # Raw hex first — useful for cross-referencing against PROTOCOL_FINDINGS.md + print(f"GetDevices raw payload: {payload.hex()}") + # Each device entry is 4 bytes: [DeviceID, DeviceType, TableID, Status] + # Structure inferred from protocol docs — verify against real hardware + entry_size = 4 + for i in range(0, len(payload) - (entry_size - 1), entry_size): + device_id = payload[i] + device_type = payload[i + 1] + table_id = payload[i + 2] + status = payload[i + 3] + print(f" Device ID={device_id} Type=0x{device_type:02x} Table={table_id} Status=0x{status:02x}") + def _next_seq(self) -> int: """Get next sequence number""" self._seq = (self._seq + 1) & 0xFFFF @@ -100,18 +115,15 @@ class OneControlClient: async def send_command(self, cmd_type: CommandType, table_id: int, payload: bytes): """Send command to device""" - # Build packet - seq = self._next_seq() - - # Track pending command - self._pending_commands[seq] = cmd_type - - packet = struct.pack(" 1: + print("Multiple OneControl devices found — set ADDRESS manually:") + for d in found: + print(f" {d.name} ({d.address})") + return + else: + # No device found via scan — set address manually if you already know it + ADDRESS = "XX:XX:XX:XX:XX:XX" + if ADDRESS == "XX:XX:XX:XX:XX:XX": + print("No device found. Set ADDRESS manually if you know the BT address.") + return + address = ADDRESS - client = OneControlClient(ADDRESS) + client = OneControlClient(address) try: + print("Connecting...") await client.connect() - print("Connected!") + print("Connected! Requesting device list...") - # Get device list await client.get_devices() - await asyncio.sleep(2) - - # Turn on light (device ID 5 as example) - await client.turn_on_light(5) - await asyncio.sleep(1) - - # Turn off light - await client.turn_off_light(5) + await asyncio.sleep(5) # Wait for all response packets finally: await client.disconnect() + print("Disconnected.") if __name__ == "__main__":