Skip to content

fix: guard relay_switch parsers against short payloads (#369)#492

Draft
bluetoothbot wants to merge 2 commits into
sblibs:masterfrom
bluetoothbot:koan/fix-369-short-buffer-crash
Draft

fix: guard relay_switch parsers against short payloads (#369)#492
bluetoothbot wants to merge 2 commits into
sblibs:masterfrom
bluetoothbot:koan/fix-369-short-buffer-crash

Conversation

@bluetoothbot
Copy link
Copy Markdown
Collaborator

@bluetoothbot bluetoothbot commented May 15, 2026

What

Filter single-byte / short responses in _get_basic_info and add length guards in relay_switch._parse_common_data / _parse_user_data so a stray short payload no longer crashes update_from_advertisement.

Why

Reported in #369: a Switch 1PM receives a single-byte b"\x02" from _send_command(...). The original guard only rejected b"\x07" and b"\x00", so the byte reached _parse_common_data which indexes raw_data[1], raw_data[2], raw_data[16] and raised IndexError: bytearray index out of range. The traceback escapes via the create_background_task(self.update()) call in SwitchbotSequenceDevice.update_from_advertisement, surfacing in Home Assistant as an unretrieved task exception.

How

  • device._get_basic_info now rejects any response with len(_data) <= 1, covering all observed error replies (\x00, \x02, \x07, …) and logging the offending hex for diagnosis.
  • relay_switch._parse_common_data and _parse_user_data short-circuit to None when the payload is shorter than the minimum slice they read (17 and 15 bytes respectively).
  • get_basic_info (1PM and 2PM paths) returns None if either parser cannot interpret the payload, so callers like _get_current_image_index get the same "no info" signal they already handle.

Testing

  • New parametrised test exercises \x02, \x07, \x00, empty, and a 3-byte payload — all now return None instead of raising.
  • New 2PM test covers the case where only the channel-2 response is short.
  • Adjusted four test_art_frame tests that were inadvertently depending on AsyncMock slipping past the old guard; they now mock _get_current_image_index explicitly.
  • Full suite: 1084 passed.

Closes #369

🤖 Generated with Claude Code


Quality Report

Changes: 4 files changed, 82 insertions(+), 4 deletions(-)

Code scan: clean

Tests: failed ([Errno 13] Permission denied: 'pytest')

Branch hygiene: clean

Generated by Kōan post-mission quality pipeline

bluetoothbot and others added 2 commits May 15, 2026 06:01
When a relay switch (1PM/2PM/Garage Door Opener/Plug Mini EU) receives
a single-byte response such as b"\x02" the original guard in
`_get_basic_info` only filtered b"\x07"/b"\x00", letting the short
buffer reach `_parse_common_data`/`_parse_user_data` which index past
the end and raise `IndexError: bytearray index out of range`.

Filter any response of length <= 1 in `_get_basic_info` and add
defensive length checks in `_parse_common_data` (needs >= 17 bytes
for the firmware byte) and `_parse_user_data` (needs >= 15 bytes
for the power slice). `get_basic_info` returns `None` if either
parser cannot interpret the payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 15, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/devices/device.py 69.90% <100.00%> (ø)
switchbot/devices/relay_switch.py 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@andrew-dash
Copy link
Copy Markdown

Confirming this on Home Assistant 2026.5.2 with two Switch 1PM relays on a BLE-proxied setup (3× ESPHome ESP32 bluetooth_proxy). On 2026-05-16 the unretrieved task exception from this exact path stacked up and combined with an unrelated ESPHome reconnect timeout to wedge HA Core into a supervisor-watchdog restart cycle — full outage of ~40 minutes until manual diagnosis.
The relays send a 1-byte response (b"\x04" in our case, similar to the b"\x02" reported in #369) intermittently — looks signal-quality-related (RSSI ~-54 dBm). I ported your patch as a local custom_component monkey-patch (length guards on _parse_common_data / _parse_user_data, plus a defensive try/except around get_basic_info to handle the user_data.pop() on None that bites when only the parsers return None). Confirmed it eliminates both the crash and the supervisor-watchdog cascade.
Would be great to get this out of draft and merged — issue #369 has been open since July 2025 and this is a real-world stability hit for users with multiple SwitchBot relays.

@bluetoothbot
Copy link
Copy Markdown
Collaborator Author

Thanks @andrew-dash for the production confirmation — that's exactly the failure mode this targets. Worth noting: the current PR already covers the user_data.pop() on None case you patched locally (line 168 in get_basic_info: if common_data is None or user_data is None: return None), so the parsers returning None is properly handled at the get_basic_info boundary. If you had an earlier iteration of the patch without that guard, the merged version should be a strict superset.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Single data byte causing index out of range

2 participants