fix: skip cached state override when plug/humidifier ignore command#503
fix: skip cached state override when plug/humidifier ignore command#503bluetoothbot wants to merge 4 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR prevents cached state from being overridden when a Plug Mini or Humidifier command is rejected/ignored by gating _override_state() / _fire_callbacks() on _check_command_result(). It also adds new test coverage for the accepted vs rejected command paths for both device classes.
Changes:
- Gate cached state override + callback firing on
retinSwitchbotPlugMini.turn_on/turn_off. - Gate cached state override + callback firing on
SwitchbotHumidifier._async_set_state/_set_level. - Add new unit tests for Plug Mini and Humidifier accepted/rejected command behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 12 comments.
| File | Description |
|---|---|
switchbot/devices/plug.py |
Only override cached isOn and fire callbacks when _check_command_result() succeeds. |
switchbot/devices/humidifier.py |
Only override cached isOn/level and fire callbacks when _check_command_result() succeeds. |
tests/test_plug.py |
Adds Plug Mini tests for accepted/rejected turn_on/turn_off. |
tests/test_humidifier.py |
Adds Humidifier tests for accepted/rejected turn_on/turn_off/set_level. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async def _async_set_state(self, state: bool) -> bool: | ||
| level = self.get_target_humidity() or 128 | ||
| result = await self._send_command(self._generate_command(on=state, level=level)) | ||
| ret = self._check_command_result(result, 0, {0x01}) | ||
| self._override_state({"isOn": state, "level": level}) | ||
| self._fire_callbacks() | ||
| if ret: |
| async def _set_level(self, level: int) -> bool: | ||
| """Set level.""" | ||
| result = await self._send_command(self._generate_command(level=level)) | ||
| ret = self._check_command_result(result, 0, {0x01}) | ||
| self._override_state({"level": level}) | ||
| self._fire_callbacks() | ||
| if ret: |
|
|
||
| assert await device.turn_on() is True | ||
|
|
||
| device._send_command.assert_called_with(plug.PLUG_ON_KEY) |
|
|
||
| assert await device.turn_off() is True | ||
|
|
||
| device._send_command.assert_called_with(plug.PLUG_OFF_KEY) |
|
|
||
| assert await device.turn_on() is False | ||
|
|
||
| device._send_command.assert_called_with(plug.PLUG_ON_KEY) |
| await device.turn_off() | ||
|
|
||
| assert device.is_on() is False |
| await device.turn_on() | ||
|
|
||
| assert device.is_on() is False |
| await device.turn_off() | ||
|
|
||
| assert device.is_on() is True |
| await device.set_level(66) | ||
|
|
||
| assert device.get_level() == 66 |
| await device.set_level(66) | ||
|
|
||
| assert device.get_level() == 33 |
PR Review — fix: skip cached state override when plug/humidifier ignore commandCorrect, well-tested core fix; the humidifier's caller-visible return contract is left half-fixed. The The gap is the humidifier: the PR description sells
🟡 Important
1. Humidifier public methods swallow the return value — fix is only half-delivered
|
Extends the bot fix from sblibs#502 to SwitchbotPlugMini and SwitchbotHumidifier: both `turn_on`/`turn_off` (and humidifier's `set_level`) ran `_override_state` unconditionally, even when `_check_command_result` returned False. A rejected/garbled command response would still flip the cached state, making HA report a state the device never reached. Gate `_override_state` and `_fire_callbacks` on the result, matching the original intent of the existing `_check_command_result` calls. Tests added: tests/test_plug.py (4 cases), tests/test_humidifier.py (6 cases) covering accepted/rejected paths for turn_on, turn_off, and set_level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rebase with requested adjustmentsBranch Changes applied
StatsActions performed
CI statusCI will be checked asynchronously. Automated by Kōan |
6f50dec to
4f64442
Compare
Close the codecov/patch gap on PR sblibs#503: the modified return statements in async_set_auto and async_set_manual had no test exercising them.
What
Gate
_override_stateon the result of_check_command_resultinSwitchbotPlugMini.turn_on/turn_offandSwitchbotHumidifier._async_set_state/_set_level. Same shape as #502 (bot), extended to the other two device classes where the existing_check_command_resultcall was discarded.Why
Audit follow-up to #502 (which fixed the bot half of #213). The same anti-pattern lives in
plug.pyandhumidifier.py:If the device returns anything other than the expected ACK byte,
retisFalsebut the cached state still gets flipped, so the library lies about state to its caller. The bot version was observed in the wild (#213); the plug/humidifier versions are the same code shape and the existing_check_command_resultcalls show the original author already intended to validate — the gate was simply missing.How
Two-line gate in each method, matching #502:
Behaviour for the accepted path is unchanged.
Testing
tests/test_plug.py(4 tests): accepted/rejected × turn_on/turn_off.tests/test_humidifier.py(6 tests): accepted/rejected × turn_on/turn_off/set_level. Neither device had a test module before.1223 passed.Quality Report
Changes: 18 files changed, 385 insertions(+), 2074 deletions(-)
Code scan: clean
Tests: passed (1311 passed)
Branch hygiene: clean
Generated by Kōan