""" Module for managing a fan via KNX. It provides functionality for * setting fan to specific speed / step * reading the current speed from KNX bus. """ from __future__ import annotations from collections.abc import Iterator from enum import Enum import logging from math import ceil from typing import TYPE_CHECKING, Any from xknx.remote_value import ( GroupAddressesType, RemoteValue, RemoteValueDptValue1Ucount, RemoteValueScaling, RemoteValueSwitch, ) from .device import Device, DeviceCallbackType if TYPE_CHECKING: from xknx.telegram import Telegram from xknx.xknx import XKNX DEFAULT_TURN_ON_SPEED = 50 logger = logging.getLogger("xknx.log") class FanSpeedMode(Enum): """Enum for setting the fan speed mode.""" PERCENT = 1 STEP = 2 class Fan(Device): """Class for managing a fan.""" def __init__( self, xknx: XKNX, name: str, group_address_speed: GroupAddressesType = None, group_address_speed_state: GroupAddressesType = None, group_address_oscillation: GroupAddressesType = None, group_address_oscillation_state: GroupAddressesType = None, group_address_switch: GroupAddressesType = None, group_address_switch_state: GroupAddressesType = None, sync_state: bool | int | float | str = True, device_updated_cb: DeviceCallbackType[Fan] | None = None, max_step: int | None = None, ) -> None: """Initialize fan class.""" super().__init__(xknx, name, device_updated_cb) self.speed: RemoteValueDptValue1Ucount | RemoteValueScaling self.mode = FanSpeedMode.STEP if max_step else FanSpeedMode.PERCENT self.max_step = max_step # If there is a dedicated switch GA, it controls the on/off behavior of the fan. # Otherwise the speed GA of the fan implicitly controls the on/off behavior instead. # `self.switch.initialized`` can be used to check which setup is used. self.switch = RemoteValueSwitch( xknx, group_address_switch, group_address_switch_state, sync_state=sync_state, device_name=self.name, feature_name="Switch", after_update_cb=self.after_update, ) if self.mode == FanSpeedMode.STEP: self.speed = RemoteValueDptValue1Ucount( xknx, group_address_speed, group_address_speed_state, sync_state=sync_state, device_name=self.name, feature_name="Speed", after_update_cb=self.after_update, ) else: self.speed = RemoteValueScaling( xknx, group_address_speed, group_address_speed_state, sync_state=sync_state, device_name=self.name, feature_name="Speed", after_update_cb=self.after_update, range_from=0, range_to=100, ) self.oscillation = RemoteValueSwitch( xknx, group_address_oscillation, group_address_oscillation_state, sync_state=sync_state, device_name=self.name, feature_name="Oscillation", after_update_cb=self.after_update, ) def _iter_remote_values(self) -> Iterator[RemoteValue[Any]]: """Iterate the devices RemoteValue classes.""" yield from (self.switch, self.speed, self.oscillation) @property def supports_oscillation(self) -> bool: """Return if fan supports oscillation.""" return self.oscillation.initialized @property def is_on(self) -> bool: """Return the current fan state of the device.""" if self.switch.initialized: return bool(self.switch.value) return bool(self.current_speed) async def turn_on(self, speed: int | None = None) -> None: """Turn on fan.""" if self.switch.initialized: self.switch.on() # For a switch GA fan, we only use an explicitly provided speed, but not # arbitrarily set a default speed here, compared to the speed GA based fans below. if speed is not None: await self.set_speed(speed) else: if speed is None: speed = ( DEFAULT_TURN_ON_SPEED if self.max_step is None # FanSpeedMode.PERCENT else ceil(self.max_step / 2) ) await self.set_speed(speed) async def turn_off(self) -> None: """Turn off fan.""" if self.switch.initialized: self.switch.off() else: await self.set_speed(0) async def set_speed(self, speed: int) -> None: """Set the fan to a designated speed.""" self.speed.set(speed) async def set_oscillation(self, oscillation: bool) -> None: """Set the fan oscillation mode on or off.""" self.oscillation.set(oscillation) def process_group_write(self, telegram: Telegram) -> None: """Process incoming and outgoing GROUP WRITE telegram.""" self.switch.process(telegram) self.speed.process(telegram) self.oscillation.process(telegram) @property def current_speed(self) -> int | None: """Return current speed of fan.""" return self.speed.value @property def current_oscillation(self) -> bool | None: """Return true if the fan is oscillating.""" return self.oscillation.value def __str__(self) -> str: """Return object as readable string.""" str_oscillation = ( f" oscillation={self.oscillation.group_addr_str()}" if self.supports_oscillation else "" ) return f''