Skip to content

Commit 1893c1f

Browse files
committed
Simplify BLE payload parsing; drop the table-driven framework
The READ_VALUES/DOOR_STATUS_PARSER table plus reflection-based _update_data_from_values was a generic parser built to decode a single field. Replace it with an explicit, typed parse: - RunChickenDoorState.from_raw() maps the raw byte safely (0=open, 1=closed, anything else -> UNKNOWN) instead of indexing a dict that raised KeyError on unexpected values. - _parse_door_state() reads the byte at a named offset with a length guard (no IndexError) and returns a RunChickenDoorState instead of a mistyped dict[str, int | float]. - Set door_state directly rather than via hasattr/setattr reflection; remove the never-read `values` catch-all from the data model and drop the unused struct import.
1 parent 6d3632a commit 1893c1f

2 files changed

Lines changed: 19 additions & 41 deletions

File tree

custom_components/run_chicken/run_chicken_ble/models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ class RunChickenDoorState(Enum):
1313
OPEN = 1
1414
CLOSED = 2
1515

16+
@classmethod
17+
def from_raw(cls, raw: int) -> RunChickenDoorState:
18+
"""Map the device's raw door-state byte (0 = open, 1 = closed) to a state."""
19+
return {0: cls.OPEN, 1: cls.CLOSED}.get(raw, cls.UNKNOWN)
20+
1621

1722
@dataclasses.dataclass
1823
class RunChickenDeviceData:
@@ -25,8 +30,6 @@ class RunChickenDeviceData:
2530
address: str = ""
2631
door_state: RunChickenDoorState = RunChickenDoorState.UNKNOWN
2732

28-
values: dict[str, int | float] = dataclasses.field(default_factory=dict)
29-
3033
def friendly_name(self) -> str:
3134
"""Generate a name for the device."""
3235
return f"Run-Chicken Door {self.name}"

custom_components/run_chicken/run_chicken_ble/parser.py

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
import logging
6-
import struct
76
from typing import TYPE_CHECKING
87

98
from bleak import BleakClient, BLEDevice
@@ -22,14 +21,9 @@
2221

2322
_LOGGER = logging.getLogger(__name__)
2423

25-
DOOR_STATUS_PARSER = {0: RunChickenDoorState.OPEN, 1: RunChickenDoorState.CLOSED}
26-
27-
READ_VALUES = {
28-
"door_state": {
29-
"format": "17xB", # Skip 17 bytes and read 1 byte as unsigned char
30-
"parser": lambda x: DOOR_STATUS_PARSER[x],
31-
}
32-
}
24+
# The read characteristic payload encodes the door state in a single byte at
25+
# this offset (0 = open, 1 = closed).
26+
DOOR_STATE_OFFSET = 17
3327

3428

3529
class RunChickenDevice:
@@ -103,47 +97,28 @@ def on_disconnect(client: BleakClient) -> None:
10397
return self._client
10498

10599
@staticmethod
106-
def _parse_payload(payload: bytearray) -> dict[str, int | float]:
100+
def _parse_door_state(payload: bytes | bytearray) -> RunChickenDoorState:
101+
"""Parse the door state out of a device payload."""
107102
_LOGGER.debug("Parsing payload: %s", payload.hex())
108-
109-
values = {}
110-
for key, config in READ_VALUES.items():
111-
value = struct.unpack(config["format"], payload[:18])[0]
112-
if "parser" in config:
113-
value = config["parser"](value)
114-
values[key] = value
115-
116-
return values
117-
118-
def _update_data_from_values(self, values: dict[str, int | float]) -> None:
119-
_LOGGER.debug("Updating device with values: %s", values)
120-
121-
for k, v in values.items():
122-
if hasattr(self._device_data, k):
123-
setattr(self._device_data, k, v)
124-
else:
125-
self._device_data.values[k] = v
103+
if len(payload) <= DOOR_STATE_OFFSET:
104+
_LOGGER.warning("Payload too short to contain door state: %s", payload.hex())
105+
return RunChickenDoorState.UNKNOWN
106+
return RunChickenDoorState.from_raw(payload[DOOR_STATE_OFFSET])
126107

127108
@retry_bluetooth_connection_error()
128-
async def _poll_values(self) -> dict[str, int | float]:
129-
"""Poll device for new values."""
109+
async def _read_payload(self) -> bytearray:
110+
"""Read the raw payload from the device's read characteristic."""
130111
char = self._client.services.get_characteristic(READ_CHAR_UUID)
131-
payload: bytearray = await self._client.read_gatt_char(char)
132-
133-
return self._parse_payload(payload)
112+
return await self._client.read_gatt_char(char)
134113

135114
def update_device_from_bytes(self, payload: bytes | bytearray) -> RunChickenDeviceData:
136115
"""Update the device from a bytes payload."""
137116
_LOGGER.debug("Updating device from bytes: %s", payload.hex())
138-
139-
values = self._parse_payload(payload)
140-
self._update_data_from_values(values)
117+
self._device_data.door_state = self._parse_door_state(payload)
141118
return self._device_data
142119

143120
async def update_device(self) -> RunChickenDeviceData:
144121
"""Connect to the device with BLE and retrieve data."""
145122
await self.ensure_client_connected()
146-
147-
values = await self._poll_values()
148-
self._update_data_from_values(values)
123+
self._device_data.door_state = self._parse_door_state(await self._read_payload())
149124
return self._device_data

0 commit comments

Comments
 (0)