Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion switchbot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
HumidifierWaterLevel,
LockStatus,
SmartThermostatRadiatorMode,
StandingFanMode,
StripLightColorMode,
SwitchbotAccountConnectionError,
SwitchbotApiError,
Expand All @@ -43,7 +44,7 @@
fetch_cloud_devices,
)
from .devices.evaporative_humidifier import SwitchbotEvaporativeHumidifier
from .devices.fan import SwitchbotFan
from .devices.fan import SwitchbotFan, SwitchbotStandingFan
from .devices.humidifier import SwitchbotHumidifier
from .devices.keypad_vision import SwitchbotKeypadVision
from .devices.light_strip import (
Expand Down Expand Up @@ -79,6 +80,7 @@
"HumidifierWaterLevel",
"LockStatus",
"SmartThermostatRadiatorMode",
"StandingFanMode",
"StripLightColorMode",
"SwitchBotAdvertisement",
"Switchbot",
Expand Down Expand Up @@ -113,6 +115,7 @@
"SwitchbotRgbicLight",
"SwitchbotRollerShade",
"SwitchbotSmartThermostatRadiator",
"SwitchbotStandingFan",
"SwitchbotStripLight3",
"SwitchbotSupportedType",
"SwitchbotSupportedType",
Expand Down
12 changes: 12 additions & 0 deletions switchbot/adv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,18 @@ class SwitchbotSupportedType(TypedDict):
"func": process_wolock_pro,
"manufacturer_id": 2409,
},
b"\x00\x11\x07\x60": {
"modelName": SwitchbotModel.STANDING_FAN,
"modelFriendlyName": "Standing Fan",
"func": process_fan,
"manufacturer_id": 2409,
},
b"\x01\x11\x07\x60": {
"modelName": SwitchbotModel.STANDING_FAN,
"modelFriendlyName": "Standing Fan",
"func": process_fan,
"manufacturer_id": 2409,
},
}

_SWITCHBOT_MODEL_TO_CHAR: defaultdict[SwitchbotModel, list[str | bytes]] = defaultdict(
Expand Down
4 changes: 3 additions & 1 deletion switchbot/const/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
HumidifierMode,
HumidifierWaterLevel,
)
from .fan import FanMode
from .fan import FanMode, StandingFanMode
from .light import (
BulbColorMode,
CeilingLightColorMode,
Expand Down Expand Up @@ -80,6 +80,7 @@ class SwitchbotModel(StrEnum):
ROLLER_SHADE = "Roller Shade"
HUBMINI_MATTER = "HubMini Matter"
CIRCULATOR_FAN = "Circulator Fan"
STANDING_FAN = "Standing Fan"
K20_VACUUM = "K20 Vacuum"
S10_VACUUM = "S10 Vacuum"
K10_VACUUM = "K10+ Vacuum"
Expand Down Expand Up @@ -126,6 +127,7 @@ class SwitchbotModel(StrEnum):
"HumidifierWaterLevel",
"LockStatus",
"SmartThermostatRadiatorMode",
"StandingFanMode",
"StripLightColorMode",
"SwitchbotAccountConnectionError",
"SwitchbotApiError",
Expand Down
12 changes: 12 additions & 0 deletions switchbot/const/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,15 @@ class FanMode(Enum):
@classmethod
def get_modes(cls) -> list[str]:
return [mode.name.lower() for mode in cls]


class StandingFanMode(Enum):
NORMAL = 1
NATURAL = 2
SLEEP = 3
BABY = 4
CUSTOM_NATURAL = 5

@classmethod
def get_modes(cls) -> list[str]:
return [mode.name.lower() for mode in cls]
108 changes: 104 additions & 4 deletions switchbot/devices/fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from typing import Any

from ..const.fan import FanMode
from ..const.fan import FanMode, StandingFanMode
from .device import (
DEVICE_GET_BASIC_SETTINGS_KEY,
SwitchbotSequenceDevice,
Expand All @@ -16,8 +16,12 @@


COMMAND_HEAD = "570f41"
COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}02010101" # H+V start
COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}02010202" # H+V stop
COMMAND_START_HORIZONTAL_OSCILLATION = f"{COMMAND_HEAD}020101ff" # H start, V keep
COMMAND_STOP_HORIZONTAL_OSCILLATION = f"{COMMAND_HEAD}020102ff" # H stop, V keep
COMMAND_START_VERTICAL_OSCILLATION = f"{COMMAND_HEAD}0201ff01" # H keep, V start
COMMAND_STOP_VERTICAL_OSCILLATION = f"{COMMAND_HEAD}0201ff02" # H keep, V stop
Comment on lines +19 to +24
COMMAND_SET_MODE = {
FanMode.NORMAL.name.lower(): f"{COMMAND_HEAD}030101ff",
FanMode.NATURAL.name.lower(): f"{COMMAND_HEAD}030102ff",
Expand All @@ -44,7 +48,9 @@ async def get_basic_info(self) -> dict[str, Any] | None:
_LOGGER.debug("data: %s", _data)
battery = _data[2] & 0b01111111
isOn = bool(_data[3] & 0b10000000)
oscillating = bool(_data[3] & 0b01100000)
oscillating_horizontal = bool(_data[3] & 0b01000000)
oscillating_vertical = bool(_data[3] & 0b00100000)
oscillating = oscillating_horizontal or oscillating_vertical
_mode = _data[8] & 0b00000111
mode = FanMode(_mode).name.lower() if 1 <= _mode <= 4 else None
speed = _data[9]
Expand All @@ -54,6 +60,8 @@ async def get_basic_info(self) -> dict[str, Any] | None:
"battery": battery,
"isOn": isOn,
"oscillating": oscillating,
"oscillating_horizontal": oscillating_horizontal,
"oscillating_vertical": oscillating_vertical,
"mode": mode,
"speed": speed,
"firmware": firmware,
Expand Down Expand Up @@ -86,6 +94,20 @@ async def set_oscillation(self, oscillating: bool) -> bool:
return await self._send_command(COMMAND_START_OSCILLATION)
return await self._send_command(COMMAND_STOP_OSCILLATION)

@update_after_operation
async def set_horizontal_oscillation(self, oscillating: bool) -> bool:
"""Send command to set fan horizontal (left-right) oscillation only."""
if oscillating:
return await self._send_command(COMMAND_START_HORIZONTAL_OSCILLATION)
return await self._send_command(COMMAND_STOP_HORIZONTAL_OSCILLATION)

@update_after_operation
async def set_vertical_oscillation(self, oscillating: bool) -> bool:
"""Send command to set fan vertical (up-down) oscillation only."""
if oscillating:
return await self._send_command(COMMAND_START_VERTICAL_OSCILLATION)
return await self._send_command(COMMAND_STOP_VERTICAL_OSCILLATION)

def get_current_percentage(self) -> Any:
"""Return cached percentage."""
return self._get_adv_value("speed")
Expand All @@ -98,6 +120,84 @@ def get_oscillating_state(self) -> Any:
"""Return cached oscillating."""
return self._get_adv_value("oscillating")

def get_horizontal_oscillating_state(self) -> Any:
"""Return cached horizontal (left-right) oscillating state."""
return self._get_adv_value("oscillating_horizontal")

def get_vertical_oscillating_state(self) -> Any:
"""Return cached vertical (up-down) oscillating state."""
return self._get_adv_value("oscillating_vertical")

def get_current_mode(self) -> Any:
"""Return cached mode."""
return self._get_adv_value("mode")


class SwitchbotStandingFan(SwitchbotFan):
"""Representation of a Switchbot Standing Fan."""

COMMAND_SET_MODE = {
StandingFanMode.NORMAL.name.lower(): f"{COMMAND_HEAD}030101ff",
StandingFanMode.NATURAL.name.lower(): f"{COMMAND_HEAD}030102ff",
StandingFanMode.SLEEP.name.lower(): f"{COMMAND_HEAD}030103",
StandingFanMode.BABY.name.lower(): f"{COMMAND_HEAD}030104",
StandingFanMode.CUSTOM_NATURAL.name.lower(): f"{COMMAND_HEAD}030105",
}
COMMAND_SET_OSCILLATION_PARAMS = f"{COMMAND_HEAD}0202"
COMMAND_SET_NIGHT_LIGHT = f"{COMMAND_HEAD}0502"

@update_after_operation
async def set_preset_mode(self, preset_mode: str) -> bool:
"""Send command to set fan preset_mode."""
return await self._send_command(self.COMMAND_SET_MODE[preset_mode])

async def get_basic_info(self) -> dict[str, Any] | None:
"""Get device basic settings."""
if not (_data := await self._get_basic_info(COMMAND_GET_BASIC_INFO)):
return None
if not (_data1 := await self._get_basic_info(DEVICE_GET_BASIC_SETTINGS_KEY)):
return None

_LOGGER.debug("data: %s", _data)
battery = _data[2] & 0b01111111
isOn = bool(_data[3] & 0b10000000)
oscillating_horizontal = bool(_data[3] & 0b01000000)
oscillating_vertical = bool(_data[3] & 0b00100000)
oscillating = oscillating_horizontal or oscillating_vertical
_mode = _data[8] & 0b00000111
mode = StandingFanMode(_mode).name.lower() if 1 <= _mode <= 5 else None
speed = _data[9]
firmware = _data1[2] / 10.0

return {
"battery": battery,
"isOn": isOn,
"oscillating": oscillating,
"oscillating_horizontal": oscillating_horizontal,
"oscillating_vertical": oscillating_vertical,
"mode": mode,
"speed": speed,
"firmware": firmware,
}

@update_after_operation
async def set_horizontal_oscillation_angle(self, angle: int) -> bool:
"""Set horizontal oscillation angle (30/60/90)."""
cmd = f"{self.COMMAND_SET_OSCILLATION_PARAMS}{angle:02X}FFFFFF"
return await self._send_command(cmd)

@update_after_operation
async def set_vertical_oscillation_angle(self, angle: int) -> bool:
"""Set vertical oscillation angle (30/60/90)."""
cmd = f"{self.COMMAND_SET_OSCILLATION_PARAMS}FFFF{angle:02X}FF"
return await self._send_command(cmd)

@update_after_operation
async def set_night_light(self, state: int) -> bool:
"""Set night light state. 1=level1, 2=level2, 3=off."""
cmd = f"{self.COMMAND_SET_NIGHT_LIGHT}{state:02X}FFFF"
return await self._send_command(cmd)

def get_night_light_state(self) -> int | None:
"""Return cached night light state."""
return self._get_adv_value("nightLight")
Loading
Loading