def test_set(self): """Test setting value.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) self.loop.run_until_complete(remote_value.down()) self.assertEqual(xknx.telegrams.qsize(), 1) telegram = xknx.telegrams.get_nowait() self.assertEqual( telegram, Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(1)), ), ) self.loop.run_until_complete(remote_value.up()) self.assertEqual(xknx.telegrams.qsize(), 1) telegram = xknx.telegrams.get_nowait() self.assertEqual( telegram, Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(0)), ), )
def test_to_knx_invert(self): """Test to_knx function with normal operation.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, invert=True) assert remote_value.to_knx( RemoteValueUpDown.Direction.UP) == DPTBinary(1) assert remote_value.to_knx( RemoteValueUpDown.Direction.DOWN) == DPTBinary(0)
def test_from_knx_invert(self): """Test from_knx function with normal operation.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, invert=True) assert remote_value.from_knx( DPTBinary(0)) == RemoteValueUpDown.Direction.DOWN assert remote_value.from_knx( DPTBinary(1)) == RemoteValueUpDown.Direction.UP
def test_to_knx_invert(self): """Test to_knx function with normal operation.""" xknx = XKNX(loop=self.loop) remote_value = RemoteValueUpDown(xknx, invert=True) self.assertEqual(remote_value.to_knx(RemoteValueUpDown.Direction.UP), DPTBinary(1)) self.assertEqual(remote_value.to_knx(RemoteValueUpDown.Direction.DOWN), DPTBinary(0))
def test_from_knx(self): """Test from_knx function with normal operation.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx) self.assertEqual(remote_value.from_knx(DPTBinary(0)), RemoteValueUpDown.Direction.UP) self.assertEqual(remote_value.from_knx(DPTBinary(1)), RemoteValueUpDown.Direction.DOWN)
def test_process(self): """Test process telegram.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) telegram = Telegram(group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)) self.assertEqual(remote_value.value, None) self.loop.run_until_complete(remote_value.process(telegram)) self.assertEqual(remote_value.value, RemoteValueUpDown.Direction.DOWN)
def __init__(self, xknx, name, group_address_long=None, group_address_short=None, group_address_position=None, group_address_position_state=None, group_address_angle=None, group_address_angle_state=None, travel_time_down=DEFAULT_TRAVEL_TIME_DOWN, travel_time_up=DEFAULT_TRAVEL_TIME_UP, invert_position=False, invert_angle=False, device_updated_cb=None): """Initialize Cover class.""" # pylint: disable=too-many-arguments super().__init__(xknx, name, device_updated_cb) self.updown = RemoteValueUpDown(xknx, group_address_long, device_name=self.name, after_update_cb=self.after_update, invert=invert_position) self.step = RemoteValueStep(xknx, group_address_short, device_name=self.name, after_update_cb=self.after_update, invert=invert_position) position_range_from = 0 if invert_position else 100 position_range_to = 100 if invert_position else 0 self.position = RemoteValueScaling(xknx, group_address_position, group_address_position_state, device_name=self.name, after_update_cb=self.after_update, range_from=position_range_from, range_to=position_range_to) angle_range_from = 0 if invert_angle else 100 angle_range_to = 100 if invert_angle else 0 self.angle = RemoteValueScaling(xknx, group_address_angle, group_address_angle_state, device_name=self.name, after_update_cb=self.after_update, range_from=angle_range_from, range_to=angle_range_to) self.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up)
def test_to_process_error(self): """Test process errornous telegram.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) with self.assertRaises(CouldNotParseTelegram): telegram = Telegram(group_address=GroupAddress("1/2/3"), payload=DPTArray(0x01)) self.loop.run_until_complete(remote_value.process(telegram)) with self.assertRaises(CouldNotParseTelegram): telegram = Telegram(group_address=GroupAddress("1/2/3"), payload=DPTBinary(3)) self.loop.run_until_complete(remote_value.process(telegram)) # pylint: disable=pointless-statement remote_value.value
def test_set(self): """Test setting value.""" xknx = XKNX(loop=self.loop) remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) self.loop.run_until_complete(asyncio.Task(remote_value.down())) self.assertEqual(xknx.telegrams.qsize(), 1) telegram = xknx.telegrams.get_nowait() self.assertEqual(telegram, Telegram(GroupAddress('1/2/3'), payload=DPTBinary(1))) self.loop.run_until_complete(asyncio.Task(remote_value.up())) self.assertEqual(xknx.telegrams.qsize(), 1) telegram = xknx.telegrams.get_nowait() self.assertEqual(telegram, Telegram(GroupAddress('1/2/3'), payload=DPTBinary(0)))
async def test_process(self): """Test process telegram.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) telegram = Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(1)), ) assert remote_value.value is None await remote_value.process(telegram) assert remote_value.value == RemoteValueUpDown.Direction.DOWN
async def test_to_process_error(self): """Test process errornous telegram.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) with pytest.raises(CouldNotParseTelegram): telegram = Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTArray(0x01)), ) await remote_value.process(telegram) with pytest.raises(CouldNotParseTelegram): telegram = Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(3)), ) await remote_value.process(telegram) # pylint: disable=pointless-statement remote_value.value
async def test_set(self): """Test setting value.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) await remote_value.down() assert xknx.telegrams.qsize() == 1 telegram = xknx.telegrams.get_nowait() assert telegram == Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(1)), ) await remote_value.up() assert xknx.telegrams.qsize() == 1 telegram = xknx.telegrams.get_nowait() assert telegram == Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(0)), )
async def test_to_process_error(self): """Test process errornous telegram.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx, group_address=GroupAddress("1/2/3")) telegram = Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTArray(0x01)), ) assert await remote_value.process(telegram) is False telegram = Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTBinary(3)), ) assert await remote_value.process(telegram) is False assert remote_value.value is None
def test_to_knx_error(self): """Test to_knx function with wrong parametern.""" xknx = XKNX() remote_value = RemoteValueUpDown(xknx) with pytest.raises(ConversionError): remote_value.to_knx(1)
def __init__( self, xknx, name, group_address_long=None, group_address_short=None, group_address_stop=None, group_address_position=None, group_address_position_state=None, group_address_angle=None, group_address_angle_state=None, travel_time_down=DEFAULT_TRAVEL_TIME_DOWN, travel_time_up=DEFAULT_TRAVEL_TIME_UP, invert_position=False, invert_angle=False, device_updated_cb=None, ): """Initialize Cover class.""" # pylint: disable=too-many-arguments 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 = RemoteValueScaling( xknx, group_address_position, group_address_position_state, device_name=self.name, feature_name="Position", after_update_cb=None, 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.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up)
def test_to_knx_error(self): """Test to_knx function with wrong parametern.""" xknx = XKNX(loop=self.loop) remote_value = RemoteValueUpDown(xknx) with self.assertRaises(ConversionError): remote_value.to_knx(1)
class Cover(Device): """Class for managing a cover.""" # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods # pylint: disable=too-many-locals # Average typical travel time of a cover DEFAULT_TRAVEL_TIME_DOWN = 22 DEFAULT_TRAVEL_TIME_UP = 22 def __init__(self, xknx, name, group_address_long=None, group_address_short=None, group_address_stop=None, group_address_position=None, group_address_position_state=None, group_address_angle=None, group_address_angle_state=None, travel_time_down=DEFAULT_TRAVEL_TIME_DOWN, travel_time_up=DEFAULT_TRAVEL_TIME_UP, invert_position=False, invert_angle=False, device_updated_cb=None): """Initialize Cover class.""" # pylint: disable=too-many-arguments 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) self.step = RemoteValueStep(xknx, group_address_short, device_name=self.name, after_update_cb=self.after_update) 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 = RemoteValueScaling(xknx, group_address_position, group_address_position_state, device_name=self.name, feature_name="Position", after_update_cb=None, 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.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) def _iter_remote_values(self): """Iterate the devices RemoteValue classes.""" yield from (self.updown, self.step, self.stop_, self.position, self.angle) @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" group_address_long = \ config.get('group_address_long') group_address_short = \ config.get('group_address_short') group_address_stop = \ config.get('group_address_stop') group_address_position = \ config.get('group_address_position') group_address_position_state = \ config.get('group_address_position_state') group_address_angle = \ config.get('group_address_angle') group_address_angle_state = \ config.get('group_address_angle_state') travel_time_down = \ config.get('travel_time_down', cls.DEFAULT_TRAVEL_TIME_DOWN) travel_time_up = \ config.get('travel_time_up', cls.DEFAULT_TRAVEL_TIME_UP) invert_position = \ config.get('invert_position', False) invert_angle = \ config.get('invert_angle', False) return cls(xknx, name, group_address_long=group_address_long, group_address_short=group_address_short, group_address_stop=group_address_stop, group_address_position=group_address_position, group_address_position_state=group_address_position_state, group_address_angle=group_address_angle, group_address_angle_state=group_address_angle_state, travel_time_down=travel_time_down, travel_time_up=travel_time_up, invert_position=invert_position, invert_angle=invert_angle) def __str__(self): """Return object as readable string.""" return '<Cover name="{0}" ' \ 'updown="{1}" ' \ 'step="{2}" ' \ 'stop="{3}" ' \ 'position="{4}" ' \ 'angle="{5}" '\ 'travel_time_down="{6}" ' \ 'travel_time_up="{7}" />' \ .format( self.name, self.updown.group_addr_str(), self.step.group_addr_str(), self.stop_.group_addr_str(), self.position.group_addr_str(), self.angle.group_addr_str(), self.travel_time_down, self.travel_time_up) async def set_down(self): """Move cover down.""" await self.updown.down() self.travelcalculator.start_travel_down() await self.after_update() async def set_up(self): """Move cover up.""" await self.updown.up() self.travelcalculator.start_travel_up() await self.after_update() async def set_short_down(self): """Move cover short down.""" await self.step.increase() async def set_short_up(self): """Move cover short up.""" await self.step.decrease() async def stop(self): """Stop cover.""" if self.stop_.writable: await self.stop_.on() elif self.step.writable: await self.step.increase() else: self.xknx.logger.warning('Stop not supported for device %s', self.get_name()) return self.travelcalculator.stop() await self.after_update() async def set_position(self, position): """Move cover to a desginated postion.""" if not self.position.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: self.xknx.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() else: await self.position.set(position) self.travelcalculator.start_travel(position) await self.after_update() async def set_angle(self, angle): """Move cover to designated angle.""" if not self.supports_angle: self.xknx.logger.warning('Angle not supported for device %s', self.get_name()) return await self.angle.set(angle) async def auto_stop_if_necessary(self): """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.writable and self.position_reached() and not self.is_open() and not self.is_closed()): await self.stop() async def do(self, action): """Execute 'do' commands.""" if action == "up": await self.set_up() elif action == "short_up": await self.set_short_up() elif action == "down": await self.set_down() elif action == "short_down": await self.set_short_down() elif action == "stop": await self.stop() else: self.xknx.logger.warning( "Could not understand action %s for device %s", action, self.get_name()) async def sync(self): """Read states of device from KNX bus.""" await self.position.read_state() await self.angle.read_state() async def process_group_write(self, telegram): """Process incoming GROUP WRITE telegram.""" if await self.updown.process(telegram): if self.updown.value == RemoteValueUpDown.Direction.UP: self.travelcalculator.start_travel_up() else: self.travelcalculator.start_travel_down() # call after_update to account for travelcalculator changes 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() if await self.position.process(telegram): # distinction between new target position and position update from bus if telegram.group_address == self.position.group_address_state: if self.is_traveling(): self.travelcalculator.update_position(self.position.value) else: self.travelcalculator.set_position(self.position.value) else: self.travelcalculator.start_travel(self.position.value) await self.after_update() await self.angle.process(telegram) def current_position(self): """Return current position of cover.""" return self.travelcalculator.current_position() def current_angle(self): """Return current tilt angle of cover.""" return self.angle.value def is_traveling(self): """Return if cover is traveling at the moment.""" return self.travelcalculator.is_traveling() def position_reached(self): """Return if cover has reached its final position.""" return self.travelcalculator.position_reached() def is_open(self): """Return if cover is open.""" return self.travelcalculator.is_open() def is_closed(self): """Return if cover is closed.""" return self.travelcalculator.is_closed() def is_opening(self): """Return if the cover is opening or not.""" return self.travelcalculator.is_opening() def is_closing(self): """Return if the cover is closing or not.""" return self.travelcalculator.is_closing() @property def supports_stop(self): """Return if cover supports manual stopping.""" return self.stop_.writable or self.step.writable @property def supports_position(self): """Return if cover supports direct positioning.""" return self.position.initialized @property def supports_angle(self): """Return if cover supports tilt angle.""" return self.angle.initialized def __eq__(self, other): """Equal operator.""" return self.__dict__ == other.__dict__
class Cover(Device): """Class for managing a cover.""" # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods # pylint: disable=too-many-locals # Average typical travel time of a cover DEFAULT_TRAVEL_TIME_DOWN = 22 DEFAULT_TRAVEL_TIME_UP = 22 def __init__(self, xknx, name, group_address_long=None, group_address_short=None, group_address_position=None, group_address_position_state=None, group_address_angle=None, group_address_angle_state=None, travel_time_down=DEFAULT_TRAVEL_TIME_DOWN, travel_time_up=DEFAULT_TRAVEL_TIME_UP, invert_position=False, invert_angle=False, device_updated_cb=None): """Initialize Cover class.""" # pylint: disable=too-many-arguments super().__init__(xknx, name, device_updated_cb) self.updown = RemoteValueUpDown(xknx, group_address_long, device_name=self.name, after_update_cb=self.after_update, invert=invert_position) self.step = RemoteValueStep(xknx, group_address_short, device_name=self.name, after_update_cb=self.after_update, invert=invert_position) position_range_from = 0 if invert_position else 100 position_range_to = 100 if invert_position else 0 self.position = RemoteValueScaling(xknx, group_address_position, group_address_position_state, device_name=self.name, after_update_cb=self.after_update, range_from=position_range_from, range_to=position_range_to) angle_range_from = 0 if invert_angle else 100 angle_range_to = 100 if invert_angle else 0 self.angle = RemoteValueScaling(xknx, group_address_angle, group_address_angle_state, device_name=self.name, after_update_cb=self.after_update, range_from=angle_range_from, range_to=angle_range_to) self.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) @classmethod def from_config(cls, xknx, name, config): """Initialize object from configuration structure.""" group_address_long = \ config.get('group_address_long') group_address_short = \ config.get('group_address_short') group_address_position = \ config.get('group_address_position') group_address_position_state = \ config.get('group_address_position_state') group_address_angle = \ config.get('group_address_angle') group_address_angle_state = \ config.get('group_address_angle_state') travel_time_down = \ config.get('travel_time_down', cls.DEFAULT_TRAVEL_TIME_DOWN) travel_time_up = \ config.get('travel_time_up', cls.DEFAULT_TRAVEL_TIME_UP) invert_position = \ config.get('invert_position', False) invert_angle = \ config.get('invert_angle', False) return cls(xknx, name, group_address_long=group_address_long, group_address_short=group_address_short, group_address_position=group_address_position, group_address_position_state=group_address_position_state, group_address_angle=group_address_angle, group_address_angle_state=group_address_angle_state, travel_time_down=travel_time_down, travel_time_up=travel_time_up, invert_position=invert_position, invert_angle=invert_angle) def has_group_address(self, group_address): """Test if device has given group address.""" return self.updown.has_group_address(group_address) \ or self.step.has_group_address(group_address) \ or self.position.has_group_address(group_address) \ or self.angle.has_group_address(group_address) def __str__(self): """Return object as readable string.""" return '<Cover name="{0}" ' \ 'updown="{1}" ' \ 'step="{2}" ' \ 'position="{3}" ' \ 'angle="{4}" '\ 'travel_time_down="{5}" ' \ 'travel_time_up="{6}" />' \ .format( self.name, self.updown.group_addr_str(), self.step.group_addr_str(), self.position.group_addr_str(), self.angle.group_addr_str(), self.travel_time_down, self.travel_time_up) async def set_down(self): """Move cover down.""" await self.updown.down() self.travelcalculator.start_travel_down() async def set_up(self): """Move cover up.""" await self.updown.up() self.travelcalculator.start_travel_up() async def set_short_down(self): """Move cover short down.""" await self.step.increase() async def set_short_up(self): """Move cover short up.""" await self.step.decrease() async def stop(self): """Stop cover.""" # Thats the KNX way of doing this. electrical engineers ... m-) await self.step.increase() self.travelcalculator.stop() async def set_position(self, position): """Move cover to a desginated postion.""" # No direct positioning group address defined if not self.position.group_address: current_position = self.current_position() if position < current_position: await self.updown.down() elif position > current_position: await self.updown.up() self.travelcalculator.start_travel(position) return await self.position.set(position) self.travelcalculator.start_travel(position) async def set_angle(self, angle): """Move cover to designated angle.""" if not self.supports_angle: self.xknx.logger.warning('Angle not supported for device %s', self.get_name()) return await self.angle.set(angle) await self.after_update() async def auto_stop_if_necessary(self): """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 (not self.position.group_address and self.position_reached() and not self.is_open() and not self.is_closed()): await self.stop() async def do(self, action): """Execute 'do' commands.""" if action == "up": await self.set_up() elif action == "short_up": await self.set_short_up() elif action == "down": await self.set_down() elif action == "short_down": await self.set_short_down() elif action == "stop": await self.stop() 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.""" if self.travelcalculator.is_traveling(): # Cover is traveling, requesting state will return false results return [] state_addresses = [] state_addresses.extend(self.position.state_addresses()) state_addresses.extend(self.angle.state_addresses()) return state_addresses async def process_group_write(self, telegram): """Process incoming GROUP WRITE telegram.""" position_processed = await self.position.process(telegram) if position_processed: self.travelcalculator.set_position(self.position.value) await self.after_update() await self.angle.process(telegram) def current_position(self): """Return current position of cover.""" return self.travelcalculator.current_position() def current_angle(self): """Return current tilt angle of cover.""" return self.angle.value def is_traveling(self): """Return if cover is traveling at the moment.""" return self.travelcalculator.is_traveling() def position_reached(self): """Return if cover has reached its final position.""" return self.travelcalculator.position_reached() def is_open(self): """Return if cover is open.""" return self.travelcalculator.is_open() def is_closed(self): """Return if cover is closed.""" return self.travelcalculator.is_closed() @property def supports_position(self): """Return if cover supports direct positioning.""" return self.position.initialized @property def supports_angle(self): """Return if cover supports tilt angle.""" return self.angle.initialized def __eq__(self, other): """Equal operator.""" return self.__dict__ == other.__dict__
def __init__( self, xknx: "XKNX", name: str, group_address_long: Optional["GroupAddressableType"] = None, group_address_short: Optional["GroupAddressableType"] = None, group_address_stop: Optional["GroupAddressableType"] = None, group_address_position: Optional["GroupAddressableType"] = None, group_address_position_state: Optional["GroupAddressableType"] = None, group_address_angle: Optional["GroupAddressableType"] = None, group_address_angle_state: Optional["GroupAddressableType"] = 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: Optional[DeviceCallbackType] = None, device_class: Optional[str] = None, ): """Initialize Cover class.""" # pylint: disable=too-many-arguments 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.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) self.device_class = device_class
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, sync_state: bool | int | float | str = True, 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, ): """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, sync_state=False, 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, sync_state=sync_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, sync_state=sync_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, sync_state=sync_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
class Cover(Device): """Class for managing a cover.""" # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods # pylint: disable=too-many-locals # Average typical travel time of a cover DEFAULT_TRAVEL_TIME_DOWN = 22 DEFAULT_TRAVEL_TIME_UP = 22 def __init__( self, xknx: "XKNX", name: str, group_address_long: Optional["GroupAddressableType"] = None, group_address_short: Optional["GroupAddressableType"] = None, group_address_stop: Optional["GroupAddressableType"] = None, group_address_position: Optional["GroupAddressableType"] = None, group_address_position_state: Optional["GroupAddressableType"] = None, group_address_angle: Optional["GroupAddressableType"] = None, group_address_angle_state: Optional["GroupAddressableType"] = 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: Optional[DeviceCallbackType] = None, device_class: Optional[str] = None, ): """Initialize Cover class.""" # pylint: disable=too-many-arguments 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.travel_time_down = travel_time_down self.travel_time_up = travel_time_up self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up) self.device_class = device_class def _iter_remote_values(self) -> Iterator["RemoteValue"]: """Iterate the devices RemoteValue classes.""" yield from ( self.updown, self.step, self.stop_, self.position_current, self.position_target, self.angle, ) @classmethod def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Cover": """Initialize object from configuration structure.""" group_address_long = config.get("group_address_long") group_address_short = config.get("group_address_short") group_address_stop = config.get("group_address_stop") group_address_position = config.get("group_address_position") group_address_position_state = config.get("group_address_position_state") group_address_angle = config.get("group_address_angle") group_address_angle_state = config.get("group_address_angle_state") travel_time_down = config.get("travel_time_down", cls.DEFAULT_TRAVEL_TIME_DOWN) travel_time_up = config.get("travel_time_up", cls.DEFAULT_TRAVEL_TIME_UP) invert_position = config.get("invert_position", False) invert_angle = config.get("invert_angle", False) device_class = config.get("device_class") return cls( xknx, name, group_address_long=group_address_long, group_address_short=group_address_short, group_address_stop=group_address_stop, group_address_position=group_address_position, group_address_position_state=group_address_position_state, group_address_angle=group_address_angle, group_address_angle_state=group_address_angle_state, travel_time_down=travel_time_down, travel_time_up=travel_time_up, invert_position=invert_position, invert_angle=invert_angle, device_class=device_class, ) def __str__(self) -> str: """Return object as readable string.""" return ( '<Cover name="{}" ' 'updown="{}" ' 'step="{}" ' 'stop="{}" ' 'position_current="{}" ' 'position_target="{}" ' 'angle="{}" ' '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.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() await self.after_update() async def set_up(self) -> None: """Move cover up.""" await self.updown.up() self.travelcalculator.start_travel_up() 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: await self.step.increase() else: logger.warning("Stop not supported for device %s", self.get_name()) return self.travelcalculator.stop() 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).""" self.travelcalculator.start_travel(self.position_target.value) 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() if self.is_traveling(): self.travelcalculator.update_position(self.position_current.value) else: self.travelcalculator.set_position(self.position_current.value) 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 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 do(self, action: str) -> None: """Execute 'do' commands.""" if action == "up": await self.set_up() elif action == "short_up": await self.set_short_up() elif action == "down": await self.set_down() elif action == "short_down": await self.set_short_down() elif action == "stop": await self.stop() else: logger.warning( "Could not understand action %s for device %s", action, self.get_name() ) 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) def current_position(self) -> Optional[int]: """Return current position of cover.""" return self.travelcalculator.current_position() def current_angle(self) -> Optional[int]: """Return current tilt angle of cover.""" return self.angle.value # type: ignore 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_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
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