| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- """Library to handle connection with Switchbot."""
- from __future__ import annotations
- import logging
- from enum import Enum
- from typing import Any, ClassVar
- from ..const.fan import (
- FanMode,
- HorizontalOscillationAngle,
- NightLightState,
- StandingFanMode,
- VerticalOscillationAngle,
- )
- from .device import (
- DEVICE_GET_BASIC_SETTINGS_KEY,
- SwitchbotSequenceDevice,
- update_after_operation,
- )
- _LOGGER = logging.getLogger(__name__)
- COMMAND_HEAD = "570f41"
- # Circulator Fan (single-axis): start/stop oscillation with V kept unchanged.
- # These also serve as the explicit horizontal-only commands since the byte
- # layout is identical.
- COMMAND_START_OSCILLATION = f"{COMMAND_HEAD}020101ff"
- COMMAND_STOP_OSCILLATION = f"{COMMAND_HEAD}020102ff"
- COMMAND_START_HORIZONTAL_OSCILLATION = COMMAND_START_OSCILLATION
- COMMAND_STOP_HORIZONTAL_OSCILLATION = COMMAND_STOP_OSCILLATION
- COMMAND_START_VERTICAL_OSCILLATION = f"{COMMAND_HEAD}0201ff01" # H keep, V start
- COMMAND_STOP_VERTICAL_OSCILLATION = f"{COMMAND_HEAD}0201ff02" # H keep, V stop
- # Standing Fan (dual-axis): start/stop both axes at once.
- COMMAND_START_OSCILLATION_ALL_AXES = f"{COMMAND_HEAD}02010101"
- COMMAND_STOP_OSCILLATION_ALL_AXES = f"{COMMAND_HEAD}02010202"
- COMMAND_SET_OSCILLATION_PARAMS = f"{COMMAND_HEAD}0202" # +angles
- COMMAND_SET_NIGHT_LIGHT = f"{COMMAND_HEAD}0502" # +state
- COMMAND_SET_MODE = {
- FanMode.NORMAL.name.lower(): f"{COMMAND_HEAD}030101ff",
- FanMode.NATURAL.name.lower(): f"{COMMAND_HEAD}030102ff",
- FanMode.SLEEP.name.lower(): f"{COMMAND_HEAD}030103",
- FanMode.BABY.name.lower(): f"{COMMAND_HEAD}030104",
- }
- COMMAND_SET_STANDING_FAN_MODE = {
- **COMMAND_SET_MODE,
- StandingFanMode.CUSTOM_NATURAL.name.lower(): f"{COMMAND_HEAD}030105",
- }
- COMMAND_SET_PERCENTAGE = f"{COMMAND_HEAD}0302" # +speed
- COMMAND_GET_BASIC_INFO = "570f428102"
- class SwitchbotFan(SwitchbotSequenceDevice):
- """Representation of a Switchbot Circulator Fan."""
- _turn_on_command = f"{COMMAND_HEAD}0101"
- _turn_off_command = f"{COMMAND_HEAD}0102"
- _mode_enum: ClassVar[type[Enum]] = FanMode
- _command_set_mode: ClassVar[dict[str, str]] = COMMAND_SET_MODE
- _command_start_oscillation: ClassVar[str] = COMMAND_START_OSCILLATION
- _command_stop_oscillation: ClassVar[str] = COMMAND_STOP_OSCILLATION
- 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_enum = self._mode_enum
- max_mode = max(m.value for m in mode_enum)
- mode = mode_enum(_mode).name.lower() if 1 <= _mode <= max_mode else None
- speed = _data[9]
- firmware = _data1[2] / 10.0
- info: dict[str, Any] = {
- "battery": battery,
- "isOn": isOn,
- "oscillating": oscillating,
- "oscillating_horizontal": oscillating_horizontal,
- "oscillating_vertical": oscillating_vertical,
- "mode": mode,
- "speed": speed,
- "firmware": firmware,
- }
- # Night light is only meaningful for models that expose it. Copy from
- # the latest advertisement parse if the parser put it there.
- night_light = self._get_adv_value("nightLight")
- if night_light is not None:
- info["nightLight"] = night_light
- return info
- async def _get_basic_info(self, cmd: str) -> bytes | None:
- """Return basic info of device."""
- _data = await self._send_command(key=cmd, retry=self._retry_count)
- if _data in (b"\x07", b"\x00"):
- _LOGGER.error("Unsuccessful, please try again")
- return None
- return _data
- @update_after_operation
- async def set_preset_mode(self, preset_mode: str) -> bool:
- """Send command to set fan preset_mode."""
- result = await self._send_command(self._command_set_mode[preset_mode])
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_percentage(self, percentage: int) -> bool:
- """Send command to set fan percentage."""
- result = await self._send_command(f"{COMMAND_SET_PERCENTAGE}{percentage:02X}")
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_oscillation(self, oscillating: bool) -> bool:
- """Send command to set fan oscillation"""
- cmd = (
- self._command_start_oscillation
- if oscillating
- else self._command_stop_oscillation
- )
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_horizontal_oscillation(self, oscillating: bool) -> bool:
- """Send command to set fan horizontal (left-right) oscillation only."""
- cmd = (
- COMMAND_START_HORIZONTAL_OSCILLATION
- if oscillating
- else COMMAND_STOP_HORIZONTAL_OSCILLATION
- )
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_vertical_oscillation(self, oscillating: bool) -> bool:
- """Send command to set fan vertical (up-down) oscillation only."""
- cmd = (
- COMMAND_START_VERTICAL_OSCILLATION
- if oscillating
- else COMMAND_STOP_VERTICAL_OSCILLATION
- )
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- def get_current_percentage(self) -> Any:
- """Return cached percentage."""
- return self._get_adv_value("speed")
- def is_on(self) -> bool | None:
- """Return fan state from cache."""
- return self._get_adv_value("isOn")
- 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 (FAN2)."""
- _mode_enum: ClassVar[type[Enum]] = StandingFanMode
- _command_set_mode: ClassVar[dict[str, str]] = COMMAND_SET_STANDING_FAN_MODE
- _command_start_oscillation: ClassVar[str] = COMMAND_START_OSCILLATION_ALL_AXES
- _command_stop_oscillation: ClassVar[str] = COMMAND_STOP_OSCILLATION_ALL_AXES
- @update_after_operation
- async def set_horizontal_oscillation_angle(
- self, angle: HorizontalOscillationAngle | int
- ) -> bool:
- """Set horizontal oscillation angle (30 / 60 / 90 degrees)."""
- value = HorizontalOscillationAngle(angle).value
- cmd = f"{COMMAND_SET_OSCILLATION_PARAMS}{value:02X}FFFFFF"
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_vertical_oscillation_angle(
- self, angle: VerticalOscillationAngle | int
- ) -> bool:
- """
- Set vertical oscillation angle (30 / 60 / 90 degrees).
- The device uses a different byte encoding on the vertical axis than
- on the horizontal one — 90° maps to byte 0x5F (95), not 0x5A (90),
- which the firmware interprets as an axis halt. Use
- `VerticalOscillationAngle` (or the raw byte values 30 / 60 / 95).
- """
- value = VerticalOscillationAngle(angle).value
- cmd = f"{COMMAND_SET_OSCILLATION_PARAMS}FFFF{value:02X}FF"
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- @update_after_operation
- async def set_night_light(self, state: NightLightState | int) -> bool:
- """Set night-light state (LEVEL_1, LEVEL_2, OFF)."""
- value = NightLightState(state).value
- cmd = f"{COMMAND_SET_NIGHT_LIGHT}{value:02X}FFFF"
- result = await self._send_command(cmd)
- return self._check_command_result(result, 0, {1})
- def get_night_light_state(self) -> int | None:
- """Return cached night light state."""
- return self._get_adv_value("nightLight")
|