"""Library to handle connection with Switchbot.""" from __future__ import annotations import logging from abc import abstractmethod from typing import Any from ..models import SwitchBotAdvertisement from .device import REQ_HEADER, SwitchbotDevice, update_after_operation # Cover keys COVER_COMMAND = "4501" # For second element of open and close arrs we should add two bytes i.e. ff00 # First byte [ff] stands for speed (00 or ff - normal, 01 - slow) * # * Only for curtains 3. For other models use ff # Second byte [00] is a command (00 - open, 64 - close) POSITION_KEYS = [ f"{REQ_HEADER}{COVER_COMMAND}0101", f"{REQ_HEADER}{COVER_COMMAND}05", # +speed ] # +actual_position STOP_KEYS = [f"{REQ_HEADER}{COVER_COMMAND}0001", f"{REQ_HEADER}{COVER_COMMAND}00ff"] COVER_EXT_SUM_KEY = f"{REQ_HEADER}460401" COVER_EXT_ADV_KEY = f"{REQ_HEADER}460402" _LOGGER = logging.getLogger(__name__) class SwitchbotBaseCover(SwitchbotDevice): """Representation of a Switchbot Cover devices for both curtains and tilt blinds.""" def __init__(self, reverse: bool, *args: Any, **kwargs: Any) -> None: """Switchbot Cover device constructor.""" super().__init__(*args, **kwargs) self._reverse = reverse self._settings: dict[str, Any] = {} self.ext_info_sum: dict[str, Any] = {} self.ext_info_adv: dict[str, Any] = {} self._is_opening: bool = False self._is_closing: bool = False async def _send_multiple_commands(self, keys: list[str]) -> bool: """Send multiple commands to device. Since we current have no way to tell which command the device needs we send both. """ final_result = False for key in keys: result = await self._send_command(key) final_result |= self._check_command_result(result, 0, {1}) return final_result @update_after_operation async def stop(self) -> bool: """Send stop command to device.""" return await self._send_multiple_commands(STOP_KEYS) @update_after_operation async def set_position(self, position: int, speed: int = 255) -> bool: """Send position command (0-100) to device. Speed 255 - normal, 1 - slow""" position = (100 - position) if self._reverse else position return await self._send_multiple_commands( [ f"{POSITION_KEYS[0]}{position:02X}", f"{POSITION_KEYS[1]}{speed:02X}{position:02X}", ] ) @abstractmethod def get_position(self) -> Any: """Return current device position.""" @abstractmethod async def get_basic_info(self) -> dict[str, Any] | None: """Get device basic settings.""" @abstractmethod async def get_extended_info_summary(self) -> dict[str, Any] | None: """Get extended info for all devices in chain.""" async def get_extended_info_adv(self) -> dict[str, Any] | None: """Get advance page info for device chain.""" _data = await self._send_command(key=COVER_EXT_ADV_KEY) if not _data: _LOGGER.error("%s: Unsuccessful, no result from device", self.name) return None if _data in (b"\x07", b"\x00"): _LOGGER.error("%s: Unsuccessful, please try again", self.name) return None _state_of_charge = [ "not_charging", "charging_by_adapter", "charging_by_solar", "fully_charged", "solar_not_charging", "charging_error", ] self.ext_info_adv["device0"] = { "battery": _data[1], "firmware": _data[2] / 10.0, "stateOfCharge": _state_of_charge[_data[3]], } # If grouped curtain device present. if _data[4]: self.ext_info_adv["device1"] = { "battery": _data[4], "firmware": _data[5] / 10.0, "stateOfCharge": _state_of_charge[_data[6]], } return self.ext_info_adv def get_light_level(self) -> Any: """Return cached light level.""" # To get actual light level call update() first. return self._get_adv_value("lightLevel") def is_reversed(self) -> bool: """Return True if curtain position is opposite from SB data.""" return self._reverse def is_calibrated(self) -> Any: """Return True curtain is calibrated.""" # To get actual light level call update() first. return self._get_adv_value("calibration") def is_opening(self) -> bool: """Return True if the curtain is opening.""" return self._is_opening def is_closing(self) -> bool: """Return True if the curtain is closing.""" return self._is_closing