"""Library to handle connection with Switchbot.""" from __future__ import annotations import logging from typing import Any from ..models import SwitchBotAdvertisement from .base_cover import CONTROL_SOURCE, ROLLERSHADE_COMMAND, SwitchbotBaseCover from .device import REQ_HEADER, SwitchbotSequenceDevice, update_after_operation _LOGGER = logging.getLogger(__name__) OPEN_KEYS = [ f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}0100", f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}0000", ] CLOSE_KEYS = [ f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}0164", f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}0064", ] POSITION_KEYS = [ f"{REQ_HEADER}{ROLLERSHADE_COMMAND}01{CONTROL_SOURCE}01", f"{REQ_HEADER}{ROLLERSHADE_COMMAND}05{CONTROL_SOURCE}", ] # +actual_position STOP_KEYS = [f"{REQ_HEADER}{ROLLERSHADE_COMMAND}00{CONTROL_SOURCE}01"] class SwitchbotRollerShade(SwitchbotBaseCover, SwitchbotSequenceDevice): """Representation of a Switchbot Roller Shade.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Switchbot roller shade constructor.""" # The position of the roller shade is saved returned with 0 = open and 100 = closed. # the definition of position is the same as in Home Assistant. self._reverse: bool = kwargs.pop("reverse_mode", True) super().__init__(self._reverse, *args, **kwargs) def _set_parsed_data( self, advertisement: SwitchBotAdvertisement, data: dict[str, Any] ) -> None: """Set data.""" in_motion = data["inMotion"] previous_position = self._get_adv_value("position") new_position = data["position"] self._update_motion_direction(in_motion, previous_position, new_position) super()._set_parsed_data(advertisement, data) @update_after_operation async def open(self, mode: int = 0) -> bool: """Send open command. 0 - performance mode, 1 - unfelt mode.""" self._is_opening = True self._is_closing = False return await self._send_multiple_commands(OPEN_KEYS) @update_after_operation async def close(self, speed: int = 0) -> bool: """Send close command. 0 - performance mode, 1 - unfelt mode.""" self._is_closing = True self._is_opening = False return await self._send_multiple_commands(CLOSE_KEYS) @update_after_operation async def stop(self) -> bool: """Send stop command to device.""" self._is_opening = self._is_closing = False return await self._send_multiple_commands(STOP_KEYS) @update_after_operation async def set_position(self, position: int, mode: int = 0) -> bool: """Send position command (0-100) to device. 0 - performance mode, 1 - unfelt mode.""" position = (100 - position) if self._reverse else position self._update_motion_direction(True, self._get_adv_value("position"), position) return await self._send_multiple_commands( [ f"{POSITION_KEYS[0]}{position:02X}", f"{POSITION_KEYS[1]}{mode:02X}{position:02X}", ] ) def get_position(self) -> Any: """Return cached position (0-100) of Curtain.""" # To get actual position call update() first. return self._get_adv_value("position") async def get_basic_info(self) -> dict[str, Any] | None: """Get device basic settings.""" if not (_data := await self._get_basic_info()): return None _position = max(min(_data[5], 100), 0) _direction_adjusted_position = (100 - _position) if self._reverse else _position _previous_position = self._get_adv_value("position") _in_motion = bool(_data[4] & 0b00000011) self._update_motion_direction( _in_motion, _previous_position, _direction_adjusted_position ) return { "battery": _data[1], "firmware": _data[2] / 10.0, "chainLength": _data[3], "openDirection": ( "clockwise" if _data[4] & 0b10000000 == 128 else "anticlockwise" ), "fault": bool(_data[4] & 0b00010000), "solarPanel": bool(_data[4] & 0b00001000), "calibration": bool(_data[4] & 0b00000100), "calibrated": bool(_data[4] & 0b00000100), "inMotion": _in_motion, "position": _direction_adjusted_position, "timers": _data[6], } def _update_motion_direction( self, in_motion: bool, previous_position: int | None, new_position: int ) -> None: """Update opening/closing status based on movement.""" if previous_position is None: return if in_motion is False: self._is_closing = self._is_opening = False return if new_position != previous_position: self._is_opening = new_position > previous_position self._is_closing = new_position < previous_position