Fix bleak 3.x compatibility and code review cleanup
- 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 <noreply@anthropic.com>
This commit is contained in:
+65
-51
@@ -41,35 +41,34 @@ class OneControlClient:
|
|||||||
self.encoder = CobsEncoder()
|
self.encoder = CobsEncoder()
|
||||||
self.decoder = CobsDecoder()
|
self.decoder = CobsDecoder()
|
||||||
self._seq = 0
|
self._seq = 0
|
||||||
self._pending_commands = {} # seq -> cmd_type
|
self._pending_commands = {} # seq -> cmd_type
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect to OneControl device"""
|
"""Connect to OneControl device"""
|
||||||
self.client = BleakClient(self.address)
|
self.client = BleakClient(self.address)
|
||||||
await self.client.connect()
|
await self.client.connect()
|
||||||
# Enable notifications
|
|
||||||
await self.client.start_notify(self.READ_CHAR, self._notification_handler)
|
await self.client.start_notify(self.READ_CHAR, self._notification_handler)
|
||||||
|
|
||||||
async def disconnect(self):
|
async def disconnect(self):
|
||||||
"""Disconnect from device"""
|
"""Disconnect from device"""
|
||||||
if self.client:
|
if self.client and self.client.is_connected:
|
||||||
await self.client.disconnect()
|
await self.client.disconnect()
|
||||||
|
|
||||||
def _notification_handler(self, sender, data):
|
def _notification_handler(self, sender, data):
|
||||||
"""Handle notifications from device"""
|
"""Handle notifications from device"""
|
||||||
try:
|
try:
|
||||||
decoded = self.decoder.decode(data)
|
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...]
|
# Response Structure: [Event(1)] [Seq(2)] [RespType(1)] [Payload...]
|
||||||
if len(decoded) < 4:
|
if len(decoded) < 4:
|
||||||
return
|
return
|
||||||
|
|
||||||
event_type = decoded[0]
|
event_type = decoded[0]
|
||||||
seq = struct.unpack("<H", decoded[1:3])[0]
|
seq = struct.unpack("<H", decoded[1:3])[0]
|
||||||
resp_type = decoded[3]
|
resp_type = decoded[3]
|
||||||
payload = decoded[4:]
|
payload = decoded[4:]
|
||||||
|
|
||||||
# 2 = DeviceCommand response
|
# 2 = DeviceCommand response
|
||||||
if event_type == 2:
|
if event_type == 2:
|
||||||
cmd_type = self._pending_commands.get(seq)
|
cmd_type = self._pending_commands.get(seq)
|
||||||
@@ -80,8 +79,7 @@ class OneControlClient:
|
|||||||
print(f"Switch Command Response (Seq {seq}): {payload.hex()}")
|
print(f"Switch Command Response (Seq {seq}): {payload.hex()}")
|
||||||
else:
|
else:
|
||||||
print(f"Response (Seq {seq}, Cmd {cmd_type}): {payload.hex()}")
|
print(f"Response (Seq {seq}, Cmd {cmd_type}): {payload.hex()}")
|
||||||
|
|
||||||
# If completed, remove from pending?
|
|
||||||
# 0x81 (129) = SuccessCompleted, 0x82 (130) = FailureCompleted
|
# 0x81 (129) = SuccessCompleted, 0x82 (130) = FailureCompleted
|
||||||
if resp_type >= 128:
|
if resp_type >= 128:
|
||||||
self._pending_commands.pop(seq, None)
|
self._pending_commands.pop(seq, None)
|
||||||
@@ -93,6 +91,23 @@ class OneControlClient:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error decoding: {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:
|
def _next_seq(self) -> int:
|
||||||
"""Get next sequence number"""
|
"""Get next sequence number"""
|
||||||
self._seq = (self._seq + 1) & 0xFFFF
|
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):
|
async def send_command(self, cmd_type: CommandType, table_id: int, payload: bytes):
|
||||||
"""Send command to device"""
|
"""Send command to device"""
|
||||||
# Build packet
|
if not self.client or not self.client.is_connected:
|
||||||
seq = self._next_seq()
|
raise RuntimeError("Not connected — call connect() first")
|
||||||
|
|
||||||
# Track pending command
|
|
||||||
self._pending_commands[seq] = cmd_type
|
|
||||||
|
|
||||||
packet = struct.pack("<HBB", seq, cmd_type, table_id) + payload
|
|
||||||
|
|
||||||
# Encode
|
seq = self._next_seq()
|
||||||
|
self._pending_commands[seq] = cmd_type
|
||||||
|
|
||||||
|
packet = struct.pack("<HBB", seq, cmd_type, table_id) + payload
|
||||||
encoded = self.encoder.encode(packet)
|
encoded = self.encoder.encode(packet)
|
||||||
|
|
||||||
# Send
|
|
||||||
await self.client.write_gatt_char(self.WRITE_CHAR, encoded)
|
await self.client.write_gatt_char(self.WRITE_CHAR, encoded)
|
||||||
print(f"Sent (Seq {seq}): {packet.hex()}")
|
print(f"Sent (Seq {seq}): {packet.hex()}")
|
||||||
|
|
||||||
@@ -135,12 +147,9 @@ class OneControlClient:
|
|||||||
payload = bytes([SwitchState.TOGGLE, device_id])
|
payload = bytes([SwitchState.TOGGLE, device_id])
|
||||||
await self.send_command(CommandType.ACTION_SWITCH, 1, payload)
|
await self.send_command(CommandType.ACTION_SWITCH, 1, payload)
|
||||||
|
|
||||||
async def control_awning(self, device_id: int, position: int):
|
async def control_awning(self, device_id: int, state: MovementState):
|
||||||
"""
|
"""Control awning/slide movement (Command 65)"""
|
||||||
Control awning/slide movement (Command 65)
|
payload = bytes([state, device_id])
|
||||||
position: 0=Retract, 1=Extend, 2=Stop
|
|
||||||
"""
|
|
||||||
payload = bytes([position, device_id])
|
|
||||||
await self.send_command(CommandType.ACTION_MOVEMENT, 1, payload)
|
await self.send_command(CommandType.ACTION_MOVEMENT, 1, payload)
|
||||||
|
|
||||||
async def set_dimmer(self, device_id: int, level: int):
|
async def set_dimmer(self, device_id: int, level: int):
|
||||||
@@ -152,52 +161,57 @@ class OneControlClient:
|
|||||||
await self.send_command(CommandType.ACTION_DIMMABLE, 1, payload)
|
await self.send_command(CommandType.ACTION_DIMMABLE, 1, payload)
|
||||||
|
|
||||||
|
|
||||||
# Scan for devices
|
|
||||||
async def scan_for_onecontrol():
|
async def scan_for_onecontrol():
|
||||||
"""Scan for OneControl devices"""
|
"""Scan for OneControl devices"""
|
||||||
print("Scanning for OneControl devices...")
|
print("Scanning for OneControl devices...")
|
||||||
devices = await BleakScanner.discover(timeout=10.0)
|
devices = await BleakScanner.discover(timeout=10.0, return_adv=True)
|
||||||
|
|
||||||
for device in devices:
|
found = []
|
||||||
# Check if device advertises our service
|
for address, (device, adv) in devices.items():
|
||||||
if device.name and "OneControl" in device.name:
|
if device.name and "OneControl" in device.name:
|
||||||
print(f"Found: {device.name} ({device.address})")
|
print(f"Found by name: {device.name} ({address})")
|
||||||
# Or check UUIDs in advertisement data
|
found.append(device)
|
||||||
if device.metadata.get("uuids"):
|
elif any("00000030-0200" in uuid.lower() for uuid in adv.service_uuids):
|
||||||
for uuid in device.metadata["uuids"]:
|
print(f"Found by service UUID: {device.name or '(unnamed)'} ({address})")
|
||||||
if "00000030-0200" in uuid:
|
found.append(device)
|
||||||
print(f"Found OneControl: {device.name} ({device.address})")
|
|
||||||
|
|
||||||
return devices
|
if not found:
|
||||||
|
print("No OneControl devices found.")
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
async def main():
|
async def main():
|
||||||
# Scan for devices
|
found = await scan_for_onecontrol()
|
||||||
await scan_for_onecontrol()
|
|
||||||
|
|
||||||
# Connect to specific device
|
if len(found) == 1:
|
||||||
ADDRESS = "XX:XX:XX:XX:XX:XX" # Your device address
|
address = found[0].address
|
||||||
|
print(f"Auto-connecting to {found[0].name} ({address})")
|
||||||
|
elif len(found) > 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:
|
try:
|
||||||
|
print("Connecting...")
|
||||||
await client.connect()
|
await client.connect()
|
||||||
print("Connected!")
|
print("Connected! Requesting device list...")
|
||||||
|
|
||||||
# Get device list
|
|
||||||
await client.get_devices()
|
await client.get_devices()
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(5) # Wait for all response packets
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
|
print("Disconnected.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user