"""Classes for VeSync Switch Devices. This module provides classes for VeSync Switch Devices: 1. VeSyncSwitch: Abstract Base class for VeSync Switch Devices. 2. VeSyncWallSwitch: Class for VeSync Wall Switch Devices ESWL01 and ESWL03. 3. VeSyncDimmerSwitch: Class for VeSync Dimmer Switch Devices ESWD16. Attributes: feature_dict (dict): Dictionary of switch models and their supported features. Defines the class to use for each switch model and the list of features switch_modules (dict): Dictionary of switch models as keys and their associated classes as string values. Note: The switch device is built from the `feature_dict` dictionary and used by the `vesync.object_factory` during initial call to pyvesync.vesync.update() and determines the class to instantiate for each switch model. These classes should not be instantiated manually. """ from __future__ import annotations import logging from dataclasses import asdict from typing import TYPE_CHECKING from typing_extensions import deprecated from pyvesync.base_devices.switch_base import VeSyncSwitch from pyvesync.const import ConnectionStatus, DeviceStatus from pyvesync.models import switch_models from pyvesync.models.bypass_models import RequestBypassV1, TimerModels from pyvesync.utils.colors import Color from pyvesync.utils.device_mixins import BypassV1Mixin, process_bypassv1_result from pyvesync.utils.helpers import Helpers, Timer, Validators if TYPE_CHECKING: from pyvesync import VeSync from pyvesync.device_map import SwitchMap from pyvesync.models.vesync_models import ResponseDeviceDetailsModel _LOGGER = logging.getLogger(__name__) class VeSyncWallSwitch(BypassV1Mixin, VeSyncSwitch): """Etekcity standard wall switch. Inherits from the BypassV1Mixin and VeSyncSwitch classes. """ __slots__ = () def __init__( self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: SwitchMap ) -> None: """Initialize Etekcity Wall Switch class. Args: details (ResponseDeviceDetailsModel): The device details. manager (VeSync): The VeSync manager. feature_map (SwitchMap): The feature map for the device. """ super().__init__(details, manager, feature_map) async def get_details(self) -> None: r_dict = await self.call_bypassv1_api( RequestBypassV1, method='deviceDetail', endpoint='deviceDetail' ) r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_dict) if r is None: return resp_model = Helpers.model_maker( _LOGGER, switch_models.ResponseSwitchDetails, 'get_details', r, self ) if resp_model is None: return result = resp_model.result if not isinstance(result, switch_models.InternalSwitchResult): _LOGGER.warning('Invalid response model for switch details') return self.state.device_status = result.deviceStatus self.state.active_time = result.activeTime self.state.connection_status = result.connectionStatus async def toggle_switch(self, toggle: bool | None = None) -> bool: """Toggle switch device.""" if toggle is None: toggle = self.state.device_status != DeviceStatus.ON toggle_str = DeviceStatus.from_bool(toggle) r_dict = await self.call_bypassv1_api( switch_models.RequestSwitchStatus, {'status': toggle_str, 'switchNo': 0}, 'deviceStatus', 'deviceStatus', ) r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_dict) if r is None: return False self.state.device_status = DeviceStatus.from_bool(toggle) self.state.connection_status = ConnectionStatus.ONLINE return True async def get_timer(self) -> None: r_dict = await self.call_bypassv1_api( TimerModels.RequestV1GetTimer, method='getTimers', endpoint='timer/getTimers' ) if r_dict is None: return result_model = process_bypassv1_result( self, _LOGGER, 'get_timer', r_dict, TimerModels.ResultV1GetTimer ) if result_model is None: return timers = result_model.timers if not isinstance(timers, list) or len(timers) == 0: _LOGGER.debug('No timers found') return if len(timers) > 1: _LOGGER.debug('More than one timer found, using first timer') timer = timers[0] if not isinstance(timer, TimerModels.TimerItemV1): _LOGGER.warning('Invalid timer model') return self.state.timer = Timer( int(timer.counterTimer), action=timer.action, id=int(timer.timerID), ) async def set_timer(self, duration: int, action: str | None = None) -> bool: if action is None: action = ( DeviceStatus.ON if self.state.device_status == DeviceStatus.OFF else DeviceStatus.OFF ) if action not in [DeviceStatus.ON, DeviceStatus.OFF]: _LOGGER.warning('Invalid action for timer - on/off') return False update_dict = { 'action': action, 'counterTime': str(duration), } r_dict = await self.call_bypassv1_api( TimerModels.RequestV1SetTime, update_dict, method='addTimer', endpoint='timer/addTimer', ) if r_dict is None: return False result_model = process_bypassv1_result( self, _LOGGER, 'set_timer', r_dict, TimerModels.ResultV1SetTimer ) if result_model is None: return False self.state.timer = Timer( int(duration), action=action, id=int(result_model.timerID), ) return True async def clear_timer(self) -> bool: if self.state.timer is None: _LOGGER.warning('No timer set, run get_timer() first.') return False update_dict = { 'timerId': str(self.state.timer.id), } r_dict = await self.call_bypassv1_api( TimerModels.RequestV1ClearTimer, update_dict, method='deleteTimer', endpoint='timer/deleteTimer', ) if r_dict is None: return False result = Helpers.process_dev_response(_LOGGER, 'clear_timer', self, r_dict) if result is None: return False self.state.timer = None return True class VeSyncDimmerSwitch(BypassV1Mixin, VeSyncSwitch): """Vesync Dimmer Switch Class with RGB Faceplate. Inherits from the BypassV1Mixin and VeSyncSwitch classes. """ __slots__ = () def __init__( self, details: ResponseDeviceDetailsModel, manager: VeSync, feature_map: SwitchMap ) -> None: """Initialize dimmer switch class. Args: details (ResponseDeviceDetailsModel): The device details. manager (VeSync): The VeSync manager. feature_map (SwitchMap): The feature map for the device. """ super().__init__(details, manager, feature_map) async def get_details(self) -> None: r_bytes = await self.call_bypassv1_api( switch_models.RequestDimmerDetails, method='deviceDetail', endpoint='deviceDetail', ) r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_bytes) if r is None: return resp_model = Helpers.model_maker( _LOGGER, switch_models.ResponseSwitchDetails, 'set_timer', r, self ) if resp_model is None: return result = resp_model.result if not isinstance(result, switch_models.InternalDimmerDetailsResult): _LOGGER.warning('Invalid response model for dimmer details') return self.state.active_time = result.activeTime self.state.connection_status = result.connectionStatus self.state.brightness = result.brightness self.state.backlight_status = result.rgbStatus new_color = result.rgbValue if isinstance(new_color, switch_models.DimmerRGB): self.state.backlight_color = Color.from_rgb( new_color.red, new_color.green, new_color.blue ) self.state.indicator_status = result.indicatorlightStatus self.state.device_status = result.deviceStatus @deprecated('switch_toggle() deprecated, use toggle_switch(toggle: bool | None)') async def switch_toggle(self, status: str) -> bool: """Toggle switch status.""" return await self.toggle_switch(status == DeviceStatus.ON) async def toggle_switch(self, toggle: bool | None = None) -> bool: if toggle is None: toggle = self.state.device_status == 'off' toggle_status = DeviceStatus.from_bool(toggle) r_bytes = await self.call_bypassv1_api( switch_models.RequestDimmerStatus, {'status': toggle_status}, 'dimmerPowerSwitchCtl', 'dimmerPowerSwitchCtl', ) r = Helpers.process_dev_response(_LOGGER, 'toggle_switch', self, r_bytes) if r is None: return False self.state.device_status = toggle_status return True async def toggle_indicator_light(self, toggle: bool | None = None) -> bool: """Toggle indicator light.""" if toggle is None: toggle = self.state.indicator_status == 'off' toggle_status = DeviceStatus.from_bool(toggle) r_bytes = await self.call_bypassv1_api( switch_models.RequestDimmerStatus, {'status': toggle_status}, 'dimmerIndicatorLightCtl', 'dimmerIndicatorLightCtl', ) r = Helpers.process_dev_response(_LOGGER, 'toggle_indicator_light', self, r_bytes) if r is None: return False self.state.indicator_status = toggle_status return True async def set_backlight_status( self, status: bool, red: int | None = None, green: int | None = None, blue: int | None = None, ) -> bool: if red is not None and blue is not None and green is not None: new_color = Color.from_rgb(red, green, blue) else: new_color = None status_str = DeviceStatus.from_bool(status) update_dict: dict[str, str | dict] = {'status': status_str.value} if new_color is not None: update_dict['rgbValue'] = asdict(new_color.rgb) r_bytes = await self.call_bypassv1_api( switch_models.RequestDimmerStatus, update_dict, 'dimmerRgbValueCtl', 'dimmerRgbValueCtl', ) r = Helpers.process_dev_response(_LOGGER, 'set_rgb_backlight', self, r_bytes) if r is None: return False self.state.backlight_status = status_str if new_color is not None: self.state.backlight_color = new_color self.state.backlight_status = status_str self.state.device_status = DeviceStatus.ON self.state.connection_status = ConnectionStatus.ONLINE return True async def set_brightness(self, brightness: int) -> bool: """Set brightness of dimmer - 1 - 100.""" if not Validators.validate_zero_to_hundred(brightness): _LOGGER.warning('Invalid brightness - must be between 0 and 100') return False r_bytes = await self.call_bypassv1_api( switch_models.RequestDimmerBrightness, {'brightness': brightness}, 'dimmerBrightnessCtl', 'dimmerBrightnessCtl', ) r = Helpers.process_dev_response(_LOGGER, 'get_details', self, r_bytes) if r is None: return False self.state.brightness = brightness self.state.device_status = DeviceStatus.ON self.state.connection_status = ConnectionStatus.ONLINE return True async def get_timer(self) -> None: r_dict = await self.call_bypassv1_api( TimerModels.RequestV1GetTimer, method='getTimers', endpoint='timer/getTimers' ) result_model = process_bypassv1_result( self, _LOGGER, 'get_timer', r_dict, TimerModels.ResultV1GetTimer ) if result_model is None: return timers = result_model.timers if not isinstance(timers, list) or len(timers) == 0: _LOGGER.info('No timers found') return if len(timers) > 1: _LOGGER.debug('More than one timer found, using first timer') timer = timers[0] if not isinstance(timer, TimerModels.TimeItemV1): _LOGGER.warning('Invalid timer model') return self.state.timer = Timer( int(timer.counterTime), action=timer.action, id=int(timer.timerID), ) async def set_timer(self, duration: int, action: str | None = None) -> bool: if action is None: action = ( DeviceStatus.ON if self.state.device_status == DeviceStatus.OFF else DeviceStatus.OFF ) if action not in [DeviceStatus.ON, DeviceStatus.OFF]: _LOGGER.warning('Invalid action for timer - on/off') return False update_dict = {'action': action, 'counterTime': str(duration), 'status': '1'} r_dict = await self.call_bypassv1_api( TimerModels.RequestV1SetTime, update_dict, method='addTimer', endpoint='timer/addTimer', ) result_model = process_bypassv1_result( self, _LOGGER, 'set_timer', r_dict, TimerModels.ResultV1SetTimer ) if result_model is None: return False self.state.timer = Timer( int(duration), action=action, id=int(result_model.timerID), ) return True async def clear_timer(self) -> bool: if self.state.timer is None: _LOGGER.debug('No timer set, run get_timer() first.') return False update_dict = {'timerId': str(self.state.timer.id), 'status': '1'} r_dict = await self.call_bypassv1_api( TimerModels.RequestV1ClearTimer, update_dict, method='deleteTimer', endpoint='timer/deleteTimer', ) result = Helpers.process_dev_response(_LOGGER, 'clear_timer', self, r_dict) if result is None: return False self.state.timer = None return True