def __init__( self, xknx: XKNX, name: str, group_address_long: GroupAddressesType | None = None, group_address_short: GroupAddressesType | None = None, group_address_stop: GroupAddressesType | None = None, group_address_position: GroupAddressesType | None = None, group_address_position_state: GroupAddressesType | None = None, group_address_angle: GroupAddressesType | None = None, group_address_angle_state: GroupAddressesType | None = None, group_address_locked_state: GroupAddressesType | None = None, travel_time_down: float = DEFAULT_TRAVEL_TIME_DOWN, travel_time_up: float = DEFAULT_TRAVEL_TIME_UP, invert_position: bool = False, invert_angle: bool = False, device_updated_cb: DeviceCallbackType | None = None, device_class: str | None = None, ): """Initialize Cover class.""" super().__init__(xknx, name, device_updated_cb) # self.after_update for position changes is called after updating the # travelcalculator (in process_group_write and set_*) - angle changes # are updated from RemoteValue objects self.updown = RemoteValueUpDown( xknx, group_address_long, device_name=self.name, after_update_cb=None, invert=invert_position, ) self.step = RemoteValueStep( xknx, group_address_short, device_name=self.name, after_update_cb=self.after_update, invert=invert_position, ) self.stop_ = RemoteValueSwitch( xknx, group_address=group_address_stop, device_name=self.name, after_update_cb=None, ) position_range_from = 100 if invert_position else 0 position_range_to = 0 if invert_position else 100 self.position_current = RemoteValueScaling( xknx, group_address_state=group_address_position_state, device_name=self.name, feature_name="Position", after_update_cb=self._current_position_from_rv, range_from=position_range_from, range_to=position_range_to, ) self.position_target = RemoteValueScaling( xknx, group_address=group_address_position, device_name=self.name, feature_name="Target position", after_update_cb=self._target_position_from_rv, range_from=position_range_from, range_to=position_range_to, ) angle_range_from = 100 if invert_angle else 0 angle_range_to = 0 if invert_angle else 100 self.angle = RemoteValueScaling( xknx, group_address_angle, group_address_angle_state, device_name=self.name, feature_name="Tilt angle", after_update_cb=self.after_update, range_from=angle_range_from, range_to=angle_range_to, ) self.locked = RemoteValueSwitch( xknx, group_address_state=group_address_locked_state, device_name=self.name, feature_name="Locked", after_update_cb=self.after_update, ) self.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) self.travel_direction_tilt: TravelStatus | None = None self.device_class = device_class
def __init__( self, xknx: XKNX, name: str, group_address_switch: GroupAddressesType | None = None, group_address_switch_state: GroupAddressesType | None = None, group_address_brightness: GroupAddressesType | None = None, group_address_brightness_state: GroupAddressesType | None = None, group_address_color: GroupAddressesType | None = None, group_address_color_state: GroupAddressesType | None = None, group_address_rgbw: GroupAddressesType | None = None, group_address_rgbw_state: GroupAddressesType | None = None, group_address_xyy_color: GroupAddressesType | None = None, group_address_xyy_color_state: GroupAddressesType | None = None, group_address_tunable_white: GroupAddressesType | None = None, group_address_tunable_white_state: GroupAddressesType | None = None, group_address_color_temperature: GroupAddressesType | None = None, group_address_color_temperature_state: GroupAddressesType | None = None, group_address_switch_red: GroupAddressesType | None = None, group_address_switch_red_state: GroupAddressesType | None = None, group_address_brightness_red: GroupAddressesType | None = None, group_address_brightness_red_state: GroupAddressesType | None = None, group_address_switch_green: GroupAddressesType | None = None, group_address_switch_green_state: GroupAddressesType | None = None, group_address_brightness_green: GroupAddressesType | None = None, group_address_brightness_green_state: GroupAddressesType | None = None, group_address_switch_blue: GroupAddressesType | None = None, group_address_switch_blue_state: GroupAddressesType | None = None, group_address_brightness_blue: GroupAddressesType | None = None, group_address_brightness_blue_state: GroupAddressesType | None = None, group_address_switch_white: GroupAddressesType | None = None, group_address_switch_white_state: GroupAddressesType | None = None, group_address_brightness_white: GroupAddressesType | None = None, group_address_brightness_white_state: GroupAddressesType | None = None, min_kelvin: int | None = None, max_kelvin: int | None = None, device_updated_cb: DeviceCallbackType | None = None, ): """Initialize Light class.""" super().__init__(xknx, name, device_updated_cb) self.switch = RemoteValueSwitch( xknx, group_address_switch, group_address_switch_state, device_name=self.name, feature_name="State", after_update_cb=self.after_update, ) self.brightness = RemoteValueScaling( xknx, group_address_brightness, group_address_brightness_state, device_name=self.name, feature_name="Brightness", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color = RemoteValueColorRGB( xknx, group_address_color, group_address_color_state, device_name=self.name, after_update_cb=self.after_update, ) self.rgbw = RemoteValueColorRGBW( xknx, group_address_rgbw, group_address_rgbw_state, device_name=self.name, after_update_cb=self.after_update, ) self._xyy_color_valid: XYYColor | None = None self.xyy_color = RemoteValueColorXYY( xknx, group_address_xyy_color, group_address_xyy_color_state, device_name=self.name, after_update_cb=self._xyy_color_from_rv, ) self.tunable_white = RemoteValueScaling( xknx, group_address_tunable_white, group_address_tunable_white_state, device_name=self.name, feature_name="Tunable white", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color_temperature = RemoteValueDpt2ByteUnsigned( xknx, group_address_color_temperature, group_address_color_temperature_state, device_name=self.name, feature_name="Color temperature", after_update_cb=self.after_update, ) self.red = _SwitchAndBrightness( xknx, self.name, "red", group_address_switch_red, group_address_switch_red_state, group_address_brightness_red, group_address_brightness_red_state, self.after_update, ) self.green = _SwitchAndBrightness( xknx, self.name, "green", group_address_switch_green, group_address_switch_green_state, group_address_brightness_green, group_address_brightness_green_state, self.after_update, ) self.blue = _SwitchAndBrightness( xknx, self.name, "blue", group_address_switch_blue, group_address_switch_blue_state, group_address_brightness_blue, group_address_brightness_blue_state, self.after_update, ) self.white = _SwitchAndBrightness( xknx, self.name, "white", group_address_switch_white, group_address_switch_white_state, group_address_brightness_white, group_address_brightness_white_state, self.after_update, ) self.min_kelvin = min_kelvin self.max_kelvin = max_kelvin
class Fan(Device): """Class for managing a fan.""" def __init__( self, xknx: XKNX, name: str, group_address_speed: GroupAddressesType | None = None, group_address_speed_state: GroupAddressesType | None = None, group_address_oscillation: GroupAddressesType | None = None, group_address_oscillation_state: GroupAddressesType | None = None, device_updated_cb: DeviceCallbackType | None = None, max_step: int | 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 self.mode == FanSpeedMode.STEP: self.speed = RemoteValueDptValue1Ucount( xknx, group_address_speed, group_address_speed_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, 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, device_name=self.name, feature_name="Oscillation", after_update_cb=self.after_update, ) def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]: """Iterate the devices RemoteValue classes.""" yield from (self.speed, self.oscillation) @property def supports_oscillation(self) -> bool: """Return if fan supports oscillation.""" return self.oscillation.initialized def __str__(self) -> str: """Return object as readable string.""" str_oscillation = ("" if not self.supports_oscillation else f" oscillation={self.oscillation.group_addr_str()}") return '<Fan name="{}" speed={}{} />'.format( self.name, self.speed.group_addr_str(), str_oscillation) async def set_speed(self, speed: int) -> None: """Set the fan to a desginated speed.""" await self.speed.set(speed) async def set_oscillation(self, oscillation: bool) -> None: """Set the fan oscillation mode on or off.""" await self.oscillation.set(oscillation) async def process_group_write(self, telegram: "Telegram") -> None: """Process incoming and outgoing GROUP WRITE telegram.""" await self.speed.process(telegram) await 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
class Cover(Device): """Class for managing a cover.""" DEFAULT_TRAVEL_TIME_DOWN = 22 DEFAULT_TRAVEL_TIME_UP = 22 def __init__( self, xknx: XKNX, name: str, group_address_long: GroupAddressesType | None = None, group_address_short: GroupAddressesType | None = None, group_address_stop: GroupAddressesType | None = None, group_address_position: GroupAddressesType | None = None, group_address_position_state: GroupAddressesType | None = None, group_address_angle: GroupAddressesType | None = None, group_address_angle_state: GroupAddressesType | None = None, group_address_locked_state: GroupAddressesType | None = None, travel_time_down: float = DEFAULT_TRAVEL_TIME_DOWN, travel_time_up: float = DEFAULT_TRAVEL_TIME_UP, invert_position: bool = False, invert_angle: bool = False, device_updated_cb: DeviceCallbackType | None = None, device_class: str | None = None, ): """Initialize Cover class.""" super().__init__(xknx, name, device_updated_cb) # self.after_update for position changes is called after updating the # travelcalculator (in process_group_write and set_*) - angle changes # are updated from RemoteValue objects self.updown = RemoteValueUpDown( xknx, group_address_long, device_name=self.name, after_update_cb=None, invert=invert_position, ) self.step = RemoteValueStep( xknx, group_address_short, device_name=self.name, after_update_cb=self.after_update, invert=invert_position, ) self.stop_ = RemoteValueSwitch( xknx, group_address=group_address_stop, device_name=self.name, after_update_cb=None, ) position_range_from = 100 if invert_position else 0 position_range_to = 0 if invert_position else 100 self.position_current = RemoteValueScaling( xknx, group_address_state=group_address_position_state, device_name=self.name, feature_name="Position", after_update_cb=self._current_position_from_rv, range_from=position_range_from, range_to=position_range_to, ) self.position_target = RemoteValueScaling( xknx, group_address=group_address_position, device_name=self.name, feature_name="Target position", after_update_cb=self._target_position_from_rv, range_from=position_range_from, range_to=position_range_to, ) angle_range_from = 100 if invert_angle else 0 angle_range_to = 0 if invert_angle else 100 self.angle = RemoteValueScaling( xknx, group_address_angle, group_address_angle_state, device_name=self.name, feature_name="Tilt angle", after_update_cb=self.after_update, range_from=angle_range_from, range_to=angle_range_to, ) self.locked = RemoteValueSwitch( xknx, group_address_state=group_address_locked_state, device_name=self.name, feature_name="Locked", after_update_cb=self.after_update, ) self.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) self.travel_direction_tilt: TravelStatus | None = None self.device_class = device_class def _iter_remote_values(self) -> Iterator[RemoteValue[Any, Any]]: """Iterate the devices RemoteValue classes.""" yield self.updown yield self.step yield self.stop_ yield self.position_current yield self.position_target yield self.angle yield self.locked @property def unique_id(self) -> str | None: """Return unique id for this device.""" return f"{self.updown.group_address}" def __str__(self) -> str: """Return object as readable string.""" return ('<Cover name="{}" ' "updown={} " "step={} " "stop_={} " "position_current={} " "position_target={} " "angle={} " "locked={} " 'travel_time_down="{}" ' 'travel_time_up="{}" />'.format( self.name, self.updown.group_addr_str(), self.step.group_addr_str(), self.stop_.group_addr_str(), self.position_current.group_addr_str(), self.position_target.group_addr_str(), self.angle.group_addr_str(), self.locked.group_addr_str(), self.travel_time_down, self.travel_time_up, )) async def set_down(self) -> None: """Move cover down.""" await self.updown.down() self.travelcalculator.start_travel_down() self.travel_direction_tilt = None await self.after_update() async def set_up(self) -> None: """Move cover up.""" await self.updown.up() self.travelcalculator.start_travel_up() self.travel_direction_tilt = None await self.after_update() async def set_short_down(self) -> None: """Move cover short down.""" await self.step.increase() async def set_short_up(self) -> None: """Move cover short up.""" await self.step.decrease() async def stop(self) -> None: """Stop cover.""" if self.stop_.writable: await self.stop_.on() elif self.step.writable: if (self.travel_direction_tilt == TravelStatus.DIRECTION_UP or self.travelcalculator.travel_direction == TravelStatus.DIRECTION_UP): await self.step.decrease() elif (self.travel_direction_tilt == TravelStatus.DIRECTION_DOWN or self.travelcalculator.travel_direction == TravelStatus.DIRECTION_DOWN): await self.step.increase() else: logger.warning("Stop not supported for device %s", self.get_name()) return self.travelcalculator.stop() self.travel_direction_tilt = None await self.after_update() async def set_position(self, position: int) -> None: """Move cover to a desginated postion.""" if not self.position_target.writable: # No direct positioning group address defined # fully open or close is always possible even if current position is not known current_position = self.current_position() if current_position is None: if position == self.travelcalculator.position_open: await self.updown.up() elif position == self.travelcalculator.position_closed: await self.updown.down() else: logger.warning( "Current position unknown. Initialize cover by moving to end position." ) return elif position < current_position: await self.updown.up() elif position > current_position: await self.updown.down() self.travelcalculator.start_travel(position) await self.after_update() else: await self.position_target.set(position) async def _target_position_from_rv(self) -> None: """Update the target postion from RemoteValue (Callback).""" new_target = self.position_target.value if new_target is not None: self.travelcalculator.start_travel(new_target) await self.after_update() async def _current_position_from_rv(self) -> None: """Update the current postion from RemoteValue (Callback).""" position_before_update = self.travelcalculator.current_position() new_position = self.position_current.value if new_position is not None: if self.is_traveling(): self.travelcalculator.update_position(new_position) else: self.travelcalculator.set_position(new_position) if position_before_update != self.travelcalculator.current_position( ): await self.after_update() async def set_angle(self, angle: int) -> None: """Move cover to designated angle.""" if not self.supports_angle: logger.warning("Angle not supported for device %s", self.get_name()) return current_angle = self.current_angle() self.travel_direction_tilt = ( TravelStatus.DIRECTION_DOWN if current_angle is not None and angle >= current_angle else TravelStatus.DIRECTION_UP) await self.angle.set(angle) async def auto_stop_if_necessary(self) -> None: """Do auto stop if necessary.""" # If device does not support auto_positioning, # we have to stop the device when position is reached, # unless device was traveling to fully open # or fully closed state. if (self.supports_stop and not self.position_target.writable and self.position_reached() and not self.is_open() and not self.is_closed()): await self.stop() async def sync(self, wait_for_result: bool = False) -> None: """Read states of device from KNX bus.""" await self.position_current.read_state(wait_for_result=wait_for_result) await self.angle.read_state(wait_for_result=wait_for_result) async def process_group_write(self, telegram: "Telegram") -> None: """Process incoming and outgoing GROUP WRITE telegram.""" # call after_update to account for travelcalculator changes if await self.updown.process(telegram): if (not self.is_opening() and self.updown.value == RemoteValueUpDown.Direction.UP): self.travelcalculator.start_travel_up() await self.after_update() elif (not self.is_closing() and self.updown.value == RemoteValueUpDown.Direction.DOWN): self.travelcalculator.start_travel_down() await self.after_update() # stop from bus if await self.stop_.process(telegram) or await self.step.process( telegram): if self.is_traveling(): self.travelcalculator.stop() await self.after_update() await self.position_current.process(telegram, always_callback=True) await self.position_target.process(telegram) await self.angle.process(telegram) await self.locked.process(telegram) def current_position(self) -> int | None: """Return current position of cover.""" return self.travelcalculator.current_position() def current_angle(self) -> int | None: """Return current tilt angle of cover.""" return self.angle.value def is_locked(self) -> bool | None: """Return if the cover is currently locked for manual movement.""" return self.locked.value def is_traveling(self) -> bool: """Return if cover is traveling at the moment.""" return self.travelcalculator.is_traveling() def position_reached(self) -> bool: """Return if cover has reached its final position.""" return self.travelcalculator.position_reached() def is_open(self) -> bool: """Return if cover is open.""" return self.travelcalculator.is_open() def is_closed(self) -> bool: """Return if cover is closed.""" return self.travelcalculator.is_closed() def is_opening(self) -> bool: """Return if the cover is opening or not.""" return self.travelcalculator.is_opening() def is_closing(self) -> bool: """Return if the cover is closing or not.""" return self.travelcalculator.is_closing() @property def supports_stop(self) -> bool: """Return if cover supports manual stopping.""" return self.stop_.writable or self.step.writable @property def supports_locked(self) -> bool: """Return if cover supports locking.""" return self.locked.initialized @property def supports_position(self) -> bool: """Return if cover supports direct positioning.""" return self.position_target.initialized @property def supports_angle(self) -> bool: """Return if cover supports tilt angle.""" return self.angle.initialized
def test_calc_100_0(self): """Test if from/to calculations work with negative range.""" self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 0), 255) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 1), 252) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 2), 250) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 3), 247) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 30), 178) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 50), 128) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 70), 76) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 97), 8) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 98), 5) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 99), 3) self.assertEqual(RemoteValueScaling._calc_to_knx(100, 0, 100), 0) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 0), 100) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 1), 100) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 2), 99) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 3), 99) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 4), 98) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 5), 98) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 76), 70) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 128), 50) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 178), 30) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 251), 2) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 252), 1) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 253), 1) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 254), 0) self.assertEqual(RemoteValueScaling._calc_from_knx(100, 0, 255), 0)
class Fan(Device): """Class for managing a fan.""" # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods def __init__(self, xknx, name, group_address_speed=None, group_address_speed_state=None, device_updated_cb=None): """Initialize fan class.""" # pylint: disable=too-many-arguments Device.__init__(self, xknx, name, device_updated_cb) self.speed = RemoteValueScaling(xknx, group_address_speed, group_address_speed_state, device_name=self.name, feature_name="Speed", after_update_cb=self.after_update, range_from=0, range_to=100) def _iter_remote_values(self): """Iterate the devices RemoteValue classes.""" yield self.speed @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" group_address_speed = \ config.get('group_address_speed') group_address_speed_state = \ config.get('group_address_speed_state') return cls(xknx, name, group_address_speed=group_address_speed, group_address_speed_state=group_address_speed_state) def __str__(self): """Return object as readable string.""" return '<Fan name="{0}" ' \ 'speed="{1}" />' \ .format( self.name, self.speed.group_addr_str()) async def set_speed(self, speed): """Set the fan to a desginated speed.""" await self.speed.set(speed) async def do(self, action): """Execute 'do' commands.""" if action.startswith("speed:"): await self.set_speed(int(action[6:])) else: self.xknx.logger.warning( "Could not understand action %s for device %s", action, self.get_name()) async def process_group_write(self, telegram): """Process incoming GROUP WRITE telegram.""" await self.speed.process(telegram) @property def current_speed(self): """Return current speed of fan.""" return self.speed.value def __eq__(self, other): """Equal operator.""" return self.__dict__ == other.__dict__
def test_calc_0_100(self): """Test if from/to calculations work range 0-100 with many test cases.""" self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 0), 0) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 1), 3) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 2), 5) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 3), 8) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 30), 76) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 50), 128) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 70), 178) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 97), 247) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 98), 250) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 99), 252) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 100, 100), 255) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 0), 0) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 1), 0) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 2), 1) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 3), 1) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 4), 2) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 5), 2) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 76), 30) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 128), 50) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 178), 70) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 251), 98) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 252), 99) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 253), 99) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 254), 100) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 100, 255), 100)
def test_calc_0_1000(self): """Test if from/to calculations work with large range.""" self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 0), 0) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 1), 0) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 2), 1) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 3), 1) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 500), 128) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 997), 254) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 998), 254) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 999), 255) self.assertEqual(RemoteValueScaling._calc_to_knx(0, 1000, 1000), 255) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 0), 0) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 1), 4) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 2), 8) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 128), 502) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 251), 984) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 252), 988) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 253), 992) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 254), 996) self.assertEqual(RemoteValueScaling._calc_from_knx(0, 1000, 255), 1000)
def test_value_unit(self): """Test for the unit_of_measurement.""" xknx = XKNX(loop=self.loop) remote_value = RemoteValueScaling(xknx) self.assertEqual(remote_value.unit_of_measurement, "%")
def __init__( self, xknx: XKNX, name: str, group_address_speed: GroupAddressesType | None = None, group_address_speed_state: GroupAddressesType | None = None, group_address_oscillation: GroupAddressesType | None = None, group_address_oscillation_state: GroupAddressesType | None = None, group_address_switch: GroupAddressesType | None = None, group_address_switch_state: GroupAddressesType | None = None, sync_state: bool | int | float | str = True, device_updated_cb: DeviceCallbackType[Fan] | None = None, max_step: int | 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 __init__( self, xknx, name, group_address_switch=None, group_address_switch_state=None, group_address_brightness=None, group_address_brightness_state=None, group_address_color=None, group_address_color_state=None, group_address_rgbw=None, group_address_rgbw_state=None, group_address_tunable_white=None, group_address_tunable_white_state=None, group_address_color_temperature=None, group_address_color_temperature_state=None, min_kelvin=None, max_kelvin=None, device_updated_cb=None, ): """Initialize Light class.""" # pylint: disable=too-many-arguments super().__init__(xknx, name, device_updated_cb) self.switch = RemoteValueSwitch( xknx, group_address_switch, group_address_switch_state, device_name=self.name, feature_name="State", after_update_cb=self.after_update, ) self.brightness = RemoteValueScaling( xknx, group_address_brightness, group_address_brightness_state, device_name=self.name, feature_name="Brightness", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color = RemoteValueColorRGB( xknx, group_address_color, group_address_color_state, device_name=self.name, after_update_cb=self.after_update, ) self.rgbw = RemoteValueColorRGBW( xknx, group_address_rgbw, group_address_rgbw_state, device_name=self.name, after_update_cb=self.after_update, ) self.tunable_white = RemoteValueScaling( xknx, group_address_tunable_white, group_address_tunable_white_state, device_name=self.name, feature_name="Tunable white", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color_temperature = RemoteValueDpt2ByteUnsigned( xknx, group_address_color_temperature, group_address_color_temperature_state, device_name=self.name, feature_name="Color temperature", after_update_cb=self.after_update, ) self.min_kelvin = min_kelvin self.max_kelvin = max_kelvin
class Light(Device): """Class for managing a light.""" # pylint: disable=too-many-locals DEFAULT_MIN_KELVIN = 2700 # 370 mireds DEFAULT_MAX_KELVIN = 6000 # 166 mireds def __init__(self, xknx, name, group_address_switch=None, group_address_switch_state=None, group_address_brightness=None, group_address_brightness_state=None, group_address_color=None, group_address_color_state=None, group_address_rgbw=None, group_address_rgbw_state=None, group_address_tunable_white=None, group_address_tunable_white_state=None, group_address_color_temperature=None, group_address_color_temperature_state=None, min_kelvin=None, max_kelvin=None, device_updated_cb=None): """Initialize Light class.""" # pylint: disable=too-many-arguments super().__init__(xknx, name, device_updated_cb) self.switch = RemoteValueSwitch(xknx, group_address_switch, group_address_switch_state, device_name=self.name, after_update_cb=self.after_update) self.brightness = RemoteValueScaling(xknx, group_address_brightness, group_address_brightness_state, device_name=self.name, after_update_cb=self.after_update, range_from=0, range_to=255) self.color = RemoteValueColorRGB(xknx, group_address_color, group_address_color_state, device_name=self.name, after_update_cb=self.after_update) self.rgbw = RemoteValueColorRGBW(xknx, group_address_rgbw, group_address_rgbw_state, device_name=self.name, after_update_cb=self.after_update) self.tunable_white = RemoteValueScaling( xknx, group_address_tunable_white, group_address_tunable_white_state, device_name=self.name, after_update_cb=self.after_update, range_from=0, range_to=255) self.color_temperature = RemoteValueDpt2ByteUnsigned( xknx, group_address_color_temperature, group_address_color_temperature_state, device_name=self.name, after_update_cb=self.after_update) self.min_kelvin = min_kelvin self.max_kelvin = max_kelvin @property def supports_brightness(self): """Return if light supports brightness.""" return self.brightness.initialized @property def supports_color(self): """Return if light supports color.""" return self.color.initialized @property def supports_rgbw(self): """Return if light supports RGBW.""" return self.rgbw.initialized @property def supports_tunable_white(self): """Return if light supports tunable white / relative color temperature.""" return self.tunable_white.initialized @property def supports_color_temperature(self): """Return if light supports absolute color temperature.""" return self.color_temperature.initialized @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" group_address_switch = \ config.get('group_address_switch') group_address_switch_state = \ config.get('group_address_switch_state') group_address_brightness = \ config.get('group_address_brightness') group_address_brightness_state = \ config.get('group_address_brightness_state') group_address_color = \ config.get('group_address_color') group_address_color_state = \ config.get('group_address_color_state') group_address_rgbw = \ config.get('group_address_rgbw') group_address_rgbw_state = \ config.get('group_address_rgbw_state') group_address_tunable_white = \ config.get('group_address_tunable_white') group_address_tunable_white_state = \ config.get('group_address_tunable_white_state') group_address_color_temperature = \ config.get('group_address_color_temperature') group_address_color_temperature_state = \ config.get('group_address_color_temperature_state') min_kelvin = \ config.get('min_kelvin', Light.DEFAULT_MIN_KELVIN) max_kelvin = \ config.get('max_kelvin', Light.DEFAULT_MAX_KELVIN) return cls( xknx, name, group_address_switch=group_address_switch, group_address_switch_state=group_address_switch_state, group_address_brightness=group_address_brightness, group_address_brightness_state=group_address_brightness_state, group_address_color=group_address_color, group_address_color_state=group_address_color_state, group_address_rgbw=group_address_rgbw, group_address_rgbw_state=group_address_rgbw_state, group_address_tunable_white=group_address_tunable_white, group_address_tunable_white_state=group_address_tunable_white_state, group_address_color_temperature=group_address_color_temperature, group_address_color_temperature_state= group_address_color_temperature_state, min_kelvin=min_kelvin, max_kelvin=max_kelvin) def has_group_address(self, group_address): """Test if device has given group address.""" return (self.switch.has_group_address(group_address) or self.brightness.has_group_address(group_address) or self.color.has_group_address(group_address) or self.rgbw.has_group_address(group_address) or self.tunable_white.has_group_address(group_address) or self.color_temperature.has_group_address(group_address)) def __str__(self): """Return object as readable string.""" str_brightness = '' if not self.supports_brightness else \ ' brightness="{0}"'.format( self.brightness.group_addr_str()) str_color = '' if not self.supports_color else \ ' color="{0}"'.format( self.color.group_addr_str()) str_rgbw = '' if not self.supports_rgbw else \ ' rgbw="{0}"'.format( self.rgbw.group_addr_str()) str_tunable_white = '' if not self.supports_tunable_white else \ ' tunable white="{0}"'.format( self.tunable_white.group_addr_str()) str_color_temperature = '' if not self.supports_color_temperature else \ ' color temperature="{0}"'.format( self.color_temperature.group_addr_str()) return '<Light name="{0}" ' \ 'switch="{1}"{2}{3}{4}{5}{6} />' \ .format( self.name, self.switch.group_addr_str(), str_brightness, str_color, str_rgbw, str_tunable_white, str_color_temperature) @property def state(self): """Return the current switch state of the device.""" # None will return False return bool(self.switch.value) async def set_on(self): """Switch light on.""" await self.switch.on() async def set_off(self): """Switch light off.""" await self.switch.off() @property def current_brightness(self): """Return current brightness of light.""" return self.brightness.value async def set_brightness(self, brightness): """Set brightness of light.""" if not self.supports_brightness: self.xknx.logger.warning("Dimming not supported for device %s", self.get_name()) return await self.brightness.set(brightness) @property def current_color(self): """ Return current color of light. If the device supports RGBW, get the current RGB+White values instead. """ if self.supports_rgbw: if not self.rgbw.value: return None, None return self.rgbw.value[:3], self.rgbw.value[3] return self.color.value, None async def set_color(self, color, white=None): """ Set color of a light device. If also the white value is given and the device supports RGBW, set all four values. """ if white is not None: if self.supports_rgbw: await self.rgbw.set(list(color) + [white]) return self.xknx.logger.warning("RGBW not supported for device %s", self.get_name()) else: if self.supports_color: await self.color.set(color) return self.xknx.logger.warning("Colors not supported for device %s", self.get_name()) @property def current_tunable_white(self): """Return current relative color temperature of light.""" return self.tunable_white.value async def set_tunable_white(self, tunable_white): """Set relative color temperature of light.""" if not self.supports_tunable_white: self.xknx.logger.warning( "Tunable white not supported for device %s", self.get_name()) return await self.tunable_white.set(tunable_white) @property def current_color_temperature(self): """Return current absolute color temperature of light.""" return self.color_temperature.value async def set_color_temperature(self, color_temperature): """Set absolute color temperature of light.""" if not self.supports_color_temperature: self.xknx.logger.warning( "Absolute Color Temperature not supported for device %s", self.get_name()) return await self.color_temperature.set(color_temperature) async def do(self, action): """Execute 'do' commands.""" if action == "on": await self.set_on() elif action == "off": await self.set_off() elif action.startswith("brightness:"): await self.set_brightness(int(action[11:])) elif action.startswith("tunable_white:"): await self.set_tunable_white(int(action[14:])) elif action.startswith("color_temperature:"): await self.set_color_temperature(int(action[18:])) else: self.xknx.logger.warning( "Could not understand action %s for device %s", action, self.get_name()) def state_addresses(self): """Return group addresses which should be requested to sync state.""" state_addresses = [] state_addresses.extend(self.switch.state_addresses()) state_addresses.extend(self.color.state_addresses()) state_addresses.extend(self.rgbw.state_addresses()) state_addresses.extend(self.brightness.state_addresses()) state_addresses.extend(self.tunable_white.state_addresses()) state_addresses.extend(self.color_temperature.state_addresses()) return state_addresses async def process_group_write(self, telegram): """Process incoming GROUP WRITE telegram.""" await self.switch.process(telegram) await self.color.process(telegram) await self.rgbw.process(telegram) await self.brightness.process(telegram) await self.tunable_white.process(telegram) await self.color_temperature.process(telegram) def __eq__(self, other): """Equal operator.""" return self.__dict__ == other.__dict__
def __init__( self, xknx: "XKNX", name: str, group_address_switch: Optional["GroupAddressableType"] = None, group_address_switch_state: Optional["GroupAddressableType"] = None, group_address_brightness: Optional["GroupAddressableType"] = None, group_address_brightness_state: Optional[ "GroupAddressableType"] = None, group_address_color: Optional["GroupAddressableType"] = None, group_address_color_state: Optional["GroupAddressableType"] = None, group_address_rgbw: Optional["GroupAddressableType"] = None, group_address_rgbw_state: Optional["GroupAddressableType"] = None, group_address_tunable_white: Optional["GroupAddressableType"] = None, group_address_tunable_white_state: Optional[ "GroupAddressableType"] = None, group_address_color_temperature: Optional[ "GroupAddressableType"] = None, group_address_color_temperature_state: Optional[ "GroupAddressableType"] = None, group_address_switch_red: Optional["GroupAddressableType"] = None, group_address_switch_red_state: Optional[ "GroupAddressableType"] = None, group_address_brightness_red: Optional["GroupAddressableType"] = None, group_address_brightness_red_state: Optional[ "GroupAddressableType"] = None, group_address_switch_green: Optional["GroupAddressableType"] = None, group_address_switch_green_state: Optional[ "GroupAddressableType"] = None, group_address_brightness_green: Optional[ "GroupAddressableType"] = None, group_address_brightness_green_state: Optional[ "GroupAddressableType"] = None, group_address_switch_blue: Optional["GroupAddressableType"] = None, group_address_switch_blue_state: Optional[ "GroupAddressableType"] = None, group_address_brightness_blue: Optional["GroupAddressableType"] = None, group_address_brightness_blue_state: Optional[ "GroupAddressableType"] = None, group_address_switch_white: Optional["GroupAddressableType"] = None, group_address_switch_white_state: Optional[ "GroupAddressableType"] = None, group_address_brightness_white: Optional[ "GroupAddressableType"] = None, group_address_brightness_white_state: Optional[ "GroupAddressableType"] = None, min_kelvin: Optional[int] = None, max_kelvin: Optional[int] = None, device_updated_cb: Optional[DeviceCallbackType] = None, ): """Initialize Light class.""" # pylint: disable=too-many-arguments super().__init__(xknx, name, device_updated_cb) self.switch = RemoteValueSwitch( xknx, group_address_switch, group_address_switch_state, device_name=self.name, feature_name="State", after_update_cb=self.after_update, ) self.brightness = RemoteValueScaling( xknx, group_address_brightness, group_address_brightness_state, device_name=self.name, feature_name="Brightness", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color = RemoteValueColorRGB( xknx, group_address_color, group_address_color_state, device_name=self.name, after_update_cb=self.after_update, ) self.rgbw = RemoteValueColorRGBW( xknx, group_address_rgbw, group_address_rgbw_state, device_name=self.name, after_update_cb=self.after_update, ) self.tunable_white = RemoteValueScaling( xknx, group_address_tunable_white, group_address_tunable_white_state, device_name=self.name, feature_name="Tunable white", after_update_cb=self.after_update, range_from=0, range_to=255, ) self.color_temperature = RemoteValueDpt2ByteUnsigned( xknx, group_address_color_temperature, group_address_color_temperature_state, device_name=self.name, feature_name="Color temperature", after_update_cb=self.after_update, ) self.red = _SwitchAndBrightness( xknx, self.name, "red", group_address_switch_red, group_address_switch_red_state, group_address_brightness_red, group_address_brightness_red_state, self.after_update, ) self.green = _SwitchAndBrightness( xknx, self.name, "green", group_address_switch_green, group_address_switch_green_state, group_address_brightness_green, group_address_brightness_green_state, self.after_update, ) self.blue = _SwitchAndBrightness( xknx, self.name, "blue", group_address_switch_blue, group_address_switch_blue_state, group_address_brightness_blue, group_address_brightness_blue_state, self.after_update, ) self.white = _SwitchAndBrightness( xknx, self.name, "white", group_address_switch_white, group_address_switch_white_state, group_address_brightness_white, group_address_brightness_white_state, self.after_update, ) self.min_kelvin = min_kelvin self.max_kelvin = max_kelvin