Skip to content

add roller mode for switch2pm#457

Open
fankai777 wants to merge 21 commits into
sblibs:masterfrom
fankai777:feat/2pm
Open

add roller mode for switch2pm#457
fankai777 wants to merge 21 commits into
sblibs:masterfrom
fankai777:feat/2pm

Conversation

@fankai777

Copy link
Copy Markdown
Contributor

Summary

Add roller/cover mode support for SwitchbotRelaySwitch2PM and fix related test cases.

Changes

Features

  • SwitchbotRelaySwitch2PM now inherits from SwitchbotBaseCover, enabling cover/roller control support.
  • Added open(), close(), stop(), set_position() commands for roller mode.
  • Added position and mode properties to SwitchbotRelaySwitch2PM.
  • Added roller mode BLE commands: COMMAND_OPEN, COMMAND_CLOSE, COMMAND_POSITION, COMMAND_STOP.

Parser

  • process_relay_switch_2pm: Added mode, position, calibration fields to both channel 1 and channel 2 advertisement data.

Refactor

  • SwitchbotBaseCover.__init__: Changed reverse to a keyword-only argument, updated all subclass call sites (curtain.py, blind_tilt.py, roller_shade.py).
  • Fixed _parse_common_data in SwitchbotRelaySwitch: sequence_number now reads from raw_data[0] (was raw_data[1]).

Tests

  • Updated test_adv_parser.py: Added expected mode, position, calibration fields to SwitchbotRelaySwitch2PM test cases.
  • Updated test_base_cover.py: Fixed SwitchbotBaseCover instantiation to use new keyword argument signature.

@codecov

codecov Bot commented Mar 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.96970% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
switchbot/devices/relay_switch.py 96.55% 2 Missing ⚠️
Files with missing lines Coverage Δ
switchbot/adv_parsers/relay_switch.py 100.00% <ø> (ø)
switchbot/devices/base_cover.py 100.00% <100.00%> (ø)
switchbot/devices/blind_tilt.py 98.64% <100.00%> (ø)
switchbot/devices/curtain.py 98.66% <100.00%> (-0.02%) ⬇️
switchbot/devices/roller_shade.py 100.00% <100.00%> (ø)
switchbot/devices/relay_switch.py 98.90% <96.55%> (-1.10%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fankai777 and others added 5 commits March 5, 2026 17:50
Small position values (< 16) produced odd-length hex strings causing
fromhex() ValueError. Format position as 02X to ensure even hex digits.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… mode

- Format position as 2-digit hex (02X) to prevent odd-length hex string
  causing fromhex() ValueError on small position values
- Add _update_motion_direction override with HA semantics (new > prev = opening)
- Convert device positions (0=open, 100=closed) to HA positions before
  direction comparison in set_position
- Fix test expectations to match device/HA semantic mapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…h2PM

The method was defined twice; the first definition was dead code that
caused 8 lines of missing coverage reported by codecov.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds roller/cover mode support for SwitchbotRelaySwitch2PM by having it additionally inherit from SwitchbotBaseCover. It also refactors the SwitchbotBaseCover constructor to accept reverse as a keyword argument, updates the advertisement parser for 2PM to include mode, position, and calibration fields, and updates tests accordingly.

Changes:

  • SwitchbotRelaySwitch2PM now inherits from both SwitchbotRelaySwitch and SwitchbotBaseCover, gaining cover/roller commands (open, close, stop, set_position) and properties (position, mode).
  • SwitchbotBaseCover.__init__ changed reverse from a positional to a keyword argument; all subclass call sites updated.
  • Advertisement parser and _parse_common_data extended with mode, position, calibration fields for 2PM devices.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
switchbot/devices/base_cover.py Refactored constructor to accept reverse as keyword-only via kwargs.pop
switchbot/devices/relay_switch.py Added cover inheritance, roller commands, position/mode properties, motion direction logic, and refactored 2PM get_basic_info
switchbot/adv_parsers/relay_switch.py Added mode, position, calibration fields to 2PM advertisement parser
switchbot/devices/curtain.py Updated super().__init__ call for new keyword arg signature
switchbot/devices/blind_tilt.py Updated super().__init__ call for new keyword arg signature
switchbot/devices/roller_shade.py Updated super().__init__ call for new keyword arg signature
tests/test_relay_switch.py Added tests for cover operations, position, mode, and motion direction
tests/test_base_cover.py Updated SwitchbotBaseCover instantiation to match new keyword arg signature
tests/test_adv_parser.py Added expected mode, position, calibration fields to 2PM test cases

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread switchbot/adv_parsers/relay_switch.py Outdated
Comment thread switchbot/devices/relay_switch.py
fankai777 and others added 4 commits April 16, 2026 11:46
… abstract methods

Fix operator precedence bug where `mfr_data[9] & 0b11110000 >> 4` was
incorrectly extracting the lower nibble instead of the upper nibble for
channel 2 mode. Add `get_position()` and `get_extended_info_summary()`
implementations to satisfy SwitchbotBaseCover abstract interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	switchbot/devices/relay_switch.py
Comment thread switchbot/devices/relay_switch.py
Delegate the basic-info and channel 1 fetch/parse back to the parent
SwitchbotRelaySwitch.get_basic_info, so 2PM only adds the channel 2
read on top. The parent's _parse_common_data already returns
calibration, mode, and position, so those fields flow through
unchanged.
@bdraco

bdraco commented Apr 23, 2026

Copy link
Copy Markdown
Member

Out of time to finish reviewing this toda. Will try to pickup reviews on this repo later this week.

bdraco added 2 commits April 23, 2026 15:58
PR sblibs#457 changed `SwitchbotBaseCover.__init__` to read `reverse` from
`**kwargs` so it can travel through the MRO when `SwitchbotRelaySwitch2PM`
inherits from both `SwitchbotRelaySwitch` and `SwitchbotBaseCover`. That
silently broke any external caller passing `reverse` as a leading
positional arg — `reverse` would be ignored and the bool would be
forwarded to `super().__init__` as `device`.

Restore backward compatibility by accepting either form: a leading
positional bool (legacy) or a `reverse=` kwarg (preferred). The bool
sniff is unambiguous because the only other candidate for the first
positional is `BLEDevice`.

Adds tests covering the legacy positional form, the kwarg form, and
the omitted-default form.
`open()`, `close()`, `set_position()` previously set `_is_opening`/
`_is_closing` *before* awaiting `_send_command`, and `stop()` cleared
them before the await. If the command raised or the result failed
`_check_command_result`, the device was left in a stale motion state
that adv updates could not correct until the next position change.

Move the flag updates after the result check using the walrus operator
so they apply only on success. Adds failure-path tests for all four
methods.
return success

@update_after_operation
async def set_position(self, position: int, mode: int = 0) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't appear to honor reverse

def channel(self) -> int:
return self._channel

def get_position(self) -> Any:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't appear to honor reverse either

return success

@update_after_operation
async def set_position(self, position: int, mode: int = 0) -> bool:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mode is unused?

"isOn": bool(mfr_data[7] & 0b01000000),
"power": parse_power_data(mfr_data, 12),
"mode": (mfr_data[9] & 0b11110000) >> 4,
"position": mfr_data[14],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uses the same position as 1: is that correct?

bdraco added 2 commits April 23, 2026 16:05
Locks in the parenthesization in `process_relay_switch_2pm` for channel 2's
`mode`. Without parens, Python's precedence parses `mfr_data[9] & 0b11110000 >> 4`
as `mfr_data[9] & (0b11110000 >> 4)` = `mfr_data[9] & 0x0F`, which silently
returns channel 1's mode. The existing 2PM adv fixtures all use `mfr_data[9] = 0`
so they cannot detect this regression.
The 2PM roller mode work refactored `_parse_common_data` and dropped the
`sequence_number` key (sourced from `raw_data[1]`). This is a silent
regression for 1PM `get_basic_info` consumers that previously read it.
Add the key back and pin it with a regression test.

@bdraco bdraco left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see review comments above

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.

3 participants