Skip to content

feat: add Circulator Fan Pro (W1160) support#508

Open
Onero-testdev wants to merge 4 commits into
sblibs:mainfrom
Onero-testdev:feature/circulator-fan-pro
Open

feat: add Circulator Fan Pro (W1160) support#508
Onero-testdev wants to merge 4 commits into
sblibs:mainfrom
Onero-testdev:feature/circulator-fan-pro

Conversation

@Onero-testdev

@Onero-testdev Onero-testdev commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for the SwitchBot Circulator Fan Pro (W1160) as an encrypted BLE device.

The Pro shares the W1071 Modern Ceiling Fan advertisement layout (which differs from the legacy Circulator Fan — battery and the fan-state byte are swapped), and uses extended commands (57 0F …) with a control-source byte for control.

Features

  • Fan: power on/off, speed (1–100), preset modes (direct / natural / sleep / hurricane), horizontal + vertical oscillation
  • Night light: two-level (high / low) on/off
  • State: parsed from the advertisement (fan on/off, mode, speed, oscillation per-axis, battery, charging, night-light state)

Implementation

  • New SwitchbotModel.CIRCULATOR_FAN_PRO (W1160000) + adv signatures b"\x00\x11\xb3@" / b"\x01\x11\xb3@"
  • New process_circulator_fan_pro advertisement parser
  • New SwitchbotCirculatorFanPro device (SwitchbotEncryptedDevice + SwitchbotFan)
  • New CirculatorFanProMode enum (mode 0x04 is hurricane, not the legacy baby)

All command frames and advertisement parsing were verified against real W1160 hardware.

🤖 Generated with Claude Code

Note from SwitchBot team

This change is developed by the official SwitchBot team. The implementation has been verified with real hardware testing on physical devices.

If you have any questions or need clarification, please reach out:

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
switchbot/__init__.py 100.00% <100.00%> (ø)
switchbot/adv_parser.py 97.82% <100.00%> (ø)
switchbot/adv_parsers/fan.py 100.00% <100.00%> (ø)
switchbot/const/__init__.py 100.00% <100.00%> (ø)
switchbot/const/fan.py 100.00% <100.00%> (ø)
switchbot/devices/device.py 69.90% <ø> (ø)
switchbot/devices/fan.py 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@bdraco bdraco closed this Jun 2, 2026
@bdraco

bdraco commented Jun 2, 2026

Copy link
Copy Markdown
Member

Waiting for a PR for switchbot to add this

@Onero-testdev

Copy link
Copy Markdown
Contributor Author

@bdraco Hello, this account is being used by a developer from Switchbot. I don't quite understand what you mean by "Waiting for a PR for Switchbot to add this." I hope you can provide more information. Thanks!

@Onero-testdev

Copy link
Copy Markdown
Contributor Author

Opened the Home Assistant core PR that consumes this: home-assistant/core#173262 (adds the Circulator Fan Pro to the switchbot integration). Draft for now — the manifest still pins PySwitchbot==2.2.0, so its CI will stay red until a release containing SwitchbotCirculatorFanPro is published. Let me know how you'd like to sequence the library release and I'll bump the pin + mark it ready.

@zerzhang zerzhang reopened this Jun 9, 2026
@bluetoothbot

Copy link
Copy Markdown
Collaborator

PR Review — feat: add Circulator Fan Pro (W1160) support

Solid, hardware-verified device addition. Merge-ready pending two minor cleanups.

Strengths:

  • Adv parser correctly mirrors the W1071/legacy layout swap (battery+charging at device_data[1], state byte at device_data[2]) and its length guard (< 10) properly covers the largest index accessed (device_data[3] = mfr_data[9]) — exactly the guard-vs-index discipline this codebase has been auditing for.
  • MRO is sound: SwitchbotEncryptedDevice, SwitchbotFan both linearize through SwitchbotDevice, so the encrypted _send_command correctly takes precedence and commands get wrapped/encrypted.
  • get_basic_info is defensively guarded (len(_data1) > 2) — an improvement over the legacy SwitchbotFan.get_basic_info, which reads _data1[2] unguarded.
  • Thorough tests: active parse with verified byte math, service-data-suffix routing, night-light bit decoding, short/None-data empty parse, command-frame assertions, and basic-info None paths. Codecov shows 100% on new lines.

Needs attention (non-blocking):

  • Redundant __init__ override that just re-forwards defaults already provided by _model.
  • set_percentage silently forces NORMAL mode — documented but worth flagging to consumers.

🟢 Suggestions

1. Redundant `__init__` override adds no behavior
switchbot/devices/fan.py:268-280

The __init__ here forwards every argument to SwitchbotEncryptedDevice.__init__ with identical defaults (model already defaults to SwitchbotModel.CIRCULATOR_FAN_PRO via the _model class attribute, which the parent's __init__ falls back to when model is None).

Why it matters: it's dead boilerplate that future readers must check for hidden logic, and it duplicates the parent signature — if the base constructor gains a parameter, this override silently drops it.

Fix: drop the override entirely. _model = SwitchbotModel.CIRCULATOR_FAN_PRO already makes SwitchbotCirculatorFanPro(device, key_id, encryption_key) work, as your own test_circulator_fan_pro_instantiation (no model=) confirms.

def __init__(self, device, key_id, encryption_key, interface=0, model=SwitchbotModel.CIRCULATOR_FAN_PRO, **kwargs):
    super().__init__(device, key_id, encryption_key, interface, model, **kwargs)

Checklist

  • Adv parser length guard covers all indexed bytes
  • MRO / encrypted _send_command resolution correct
  • Command frames and getters semantically consistent
  • New public symbols exported (init/all)
  • Test coverage incl. edge cases (None/short data)
  • No dead/redundant code — suggestion #1
  • PR description matches diff

Automated review by Kōan (Claude) HEAD=e8b4508 3 min 31s

bluetoothbot
bluetoothbot previously approved these changes Jun 20, 2026

@bluetoothbot bluetoothbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No blocking issues found.

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

Adds SwitchBot Circulator Fan Pro (W1160) support to the library by introducing a new encrypted fan device class and advertisement parsing so the device can be discovered and controlled over BLE.

Changes:

  • Added Circulator Fan Pro model/exports/constants and API model mapping (W1160000).
  • Added W1160 advertisement routing + parser for its modern fan broadcast layout.
  • Added SwitchbotCirculatorFanPro device implementation plus dedicated unit tests for parsing and commands.

Reviewed changes

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

Show a summary per file
File Description
tests/test_fan.py Adds unit tests and helpers for Circulator Fan Pro commands/state getters.
tests/test_adv_parser.py Adds advertisement parsing tests and model routing verification for W1160.
switchbot/devices/fan.py Introduces SwitchbotCirculatorFanPro and a fan_modes convenience property.
switchbot/devices/device.py Maps SwitchBot API model string W1160000 to the new enum model.
switchbot/const/fan.py Adds CirculatorFanProMode enum.
switchbot/const/__init__.py Exports CirculatorFanProMode and adds CIRCULATOR_FAN_PRO to SwitchbotModel.
switchbot/adv_parsers/fan.py Adds process_circulator_fan_pro advertisement parser.
switchbot/adv_parser.py Adds W1160 service-data suffix signatures to route to the new parser.
switchbot/__init__.py Exports SwitchbotCirculatorFanPro and CirculatorFanProMode.

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

Comment thread switchbot/devices/fan.py
Comment on lines +306 to +314
async def set_percentage(self, percentage: int) -> bool:
"""
Set the fan speed (1-100).

Speed lives in byte7 of the 0x11 power command and only applies in
direct mode, so this sends "on + direct mode + speed".
"""
result = await self._send_command(f"570f4111290101{percentage:02X}")
return self._check_command_result(result, 0, {1})
Comment thread switchbot/devices/fan.py
Comment on lines +243 to +248
The Pro uses extended commands (``57 0F <subcmd> …``) with a control-source
byte (0x29 = Home Assistant), wrapped in the encrypted command shell, so it
extends SwitchbotEncryptedDevice. Fan power uses subcommand 0x41 (open/close
sub-op 0x11); the night light uses subcommand 0x96. The night light command
is on/off only (the device exposes two read-only brightness levels in its
advertisement, but they are not separately settable here).
Comment thread tests/test_fan.py
Comment on lines +387 to +400
def create_circulator_fan_pro_for_testing(init_data: dict | None = None):
"""Create an encrypted SwitchbotCirculatorFanPro instance for testing."""
ble_device = generate_ble_device("aa:bb:cc:dd:ee:ff", "any")
fan_device = SwitchbotCirculatorFanPro(
ble_device,
"ff",
"ffffffffffffffffffffffffffffffff",
model=SwitchbotModel.CIRCULATOR_FAN_PRO,
)
fan_device.update_from_advertisement(make_advertisement_data(ble_device, init_data))
fan_device._send_command = AsyncMock()
fan_device._check_command_result = MagicMock()
fan_device.update = AsyncMock()
return fan_device
Onero-testdev and others added 3 commits June 25, 2026 16:40
Add the SwitchBot Circulator Fan Pro (W1160) as an encrypted BLE device:
fan power/speed/preset-mode (direct/natural/sleep/hurricane), horizontal and
vertical oscillation, and a two-level night light. State is parsed from the
W1071-style advertisement (battery/fan-state swapped vs the legacy fan).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

5 participants