def test_cemi_frame(self): """Test string representation of KNX/IP CEMI Frame.""" xknx = XKNX(loop=self.loop) cemi_frame = CEMIFrame(xknx) cemi_frame.src_addr = GroupAddress("1/2/3") cemi_frame.telegram = Telegram( group_address=GroupAddress('1/2/5'), payload=DPTBinary(7)) self.assertEqual( str(cemi_frame), '<CEMIFrame SourceAddress="GroupAddress("1/2/3")" DestinationAddress="GroupAddress("1/2/5")" Flags="1011110011100000" Command="APCIC' 'ommand.GROUP_WRITE" payload="<DPTBinary value="7" />" />')
def test_telegram_set(self): """Test parsing and streaming CEMIFrame KNX/IP packet with DPTArray/DPTTime as payload.""" telegram = Telegram( destination_address=GroupAddress(337), payload=GroupValueWrite( DPTArray(DPTTime().to_knx(time.strptime( "13:23:42", "%H:%M:%S")))), ) cemi = CEMIFrame(src_addr=IndividualAddress("1.2.2")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(cemi=cemi) knxipframe = KNXIPFrame.init_from_body(routing_indication) raw = bytes.fromhex( "06 10 05 30 00 14 29 00 bc d0 12 02 01 51 04 00 80 0d 17 2a") assert knxipframe.header.to_knx() == raw[0:6] assert knxipframe.body.to_knx() == raw[6:] assert knxipframe.to_knx() == raw
def create_knxipframe(self) -> KNXIPFrame: """Create KNX/IP Frame object to be sent to device.""" cemi = CEMIFrame.init_from_telegram( telegram=self.telegram, code=CEMIMessageCode.L_DATA_REQ, src_addr=self.src_address, ) tunnelling_request = TunnellingRequest( communication_channel_id=self.communication_channel_id, sequence_counter=self.sequence_counter, cemi=cemi, ) return KNXIPFrame.init_from_body(tunnelling_request)
async def test_tunnel_request_received_callback( self, send_cemi_mock, send_ack_mock, ): """Test Tunnel for responding to L_DATA.req with confirmation and indication.""" # L_Data.req T_Connect from 1.0.250 to 1.0.255 (xknx tunnel endpoint) - ETS Line-Scan # communication_channel_id: 0x02 sequence_counter: 0x81 raw_req = bytes.fromhex("0610 0420 0014 04 02 81 00 1100b06010fa10ff0080") _cemi = CEMIFrame() _cemi.from_knx(raw_req[10:]) test_telegram = _cemi.telegram test_telegram.direction = TelegramDirection.INCOMING response_telegram = Telegram(source_address=self.tunnel._src_address) async def tg_received_mock(telegram): """Mock for telegram_received_callback.""" assert telegram == test_telegram return [response_telegram] self.tunnel.telegram_received_callback = tg_received_mock self.tunnel.transport.data_received_callback(raw_req, ("192.168.1.2", 3671)) await asyncio.sleep(0) confirmation_cemi = _cemi confirmation_cemi.code = CEMIMessageCode.L_DATA_CON response_cemi = CEMIFrame.init_from_telegram( response_telegram, code=CEMIMessageCode.L_DATA_IND, src_addr=self.tunnel._src_address, ) assert send_cemi_mock.call_args_list == [ call(confirmation_cemi), call(response_cemi), ] send_ack_mock.assert_called_once_with(raw_req[7], raw_req[8])
def test_maximum_apci(self): """Test parsing and streaming CEMIFrame KNX/IP packet, testing maximum APCI.""" telegram = Telegram( destination_address=GroupAddress(337), payload=GroupValueWrite(DPTBinary(DPTBinary.APCI_MAX_VALUE)), source_address=IndividualAddress("1.3.1"), ) xknx = XKNX() cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.3.1")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(xknx, cemi=cemi) knxipframe = KNXIPFrame.init_from_body(routing_indication) raw = bytes.fromhex("0610053000112900BCD0130101510100BF") self.assertEqual(knxipframe.to_knx(), list(raw)) knxipframe2 = KNXIPFrame(xknx) knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION) knxipframe2.from_knx(knxipframe.to_knx()) self.assertEqual(knxipframe2.body.cemi.telegram, telegram)
def test_end_to_end_group_write_binary_on(self): """Test parsing and streaming CEMIFrame KNX/IP packet, switch on light in my kitchen.""" # Switch on Kitchen-L1 raw = bytes.fromhex("0610053000112900BCD0FFF90149010081") xknx = XKNX() knxipframe = KNXIPFrame(xknx) knxipframe.from_knx(raw) telegram = knxipframe.body.cemi.telegram assert telegram == Telegram( destination_address=GroupAddress("329"), payload=GroupValueWrite(DPTBinary(1)), source_address=IndividualAddress("15.15.249"), ) cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(xknx, cemi=cemi) knxipframe2 = KNXIPFrame.init_from_body(routing_indication) assert knxipframe2.header.to_knx() == list(raw[0:6]) assert knxipframe2.body.to_knx() == list(raw[6:]) assert knxipframe2.to_knx() == list(raw)
def test_end_to_end_group_response(self): """Test parsing and streaming CEMIFrame KNX/IP packet, group response.""" # Incoming state raw = bytes.fromhex("0610053000112900BCD013010188010041") xknx = XKNX() knxipframe = KNXIPFrame(xknx) knxipframe.from_knx(raw) telegram = knxipframe.body.cemi.telegram assert telegram == Telegram( destination_address=GroupAddress("392"), payload=GroupValueResponse(DPTBinary(1)), source_address=IndividualAddress("1.3.1"), ) cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.3.1")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(xknx, cemi=cemi) knxipframe2 = KNXIPFrame.init_from_body(routing_indication) assert knxipframe2.header.to_knx() == list(raw[0:6]) assert knxipframe2.body.to_knx() == list(raw[6:]) assert knxipframe2.to_knx() == list(raw)
def test_end_to_end_group_read(self): """Test parsing and streaming CEMIFrame KNX/IP packet, group read.""" # State request raw = bytes.fromhex("0610053000112900BCD0FFF901B8010000") xknx = XKNX() knxipframe = KNXIPFrame(xknx) knxipframe.from_knx(raw) telegram = knxipframe.body.cemi.telegram assert telegram == Telegram( destination_address=GroupAddress("440"), payload=GroupValueRead(), source_address=IndividualAddress("15.15.249"), ) cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(xknx, cemi=cemi) knxipframe2 = KNXIPFrame.init_from_body(routing_indication) assert knxipframe2.header.to_knx() == list(raw[0:6]) assert knxipframe2.body.to_knx() == list(raw[6:]) assert knxipframe2.to_knx() == list(raw)
def test_end_to_end_group_write_2bytes(self): """Test parsing and streaming CEMIFrame KNX/IP packet, setting value of thermostat.""" # Incoming Temperature from thermostat raw = bytes.fromhex("0610053000132900BCD01402080103008007C1") xknx = XKNX() knxipframe = KNXIPFrame(xknx) knxipframe.from_knx(raw) telegram = knxipframe.body.cemi.telegram assert telegram == Telegram( destination_address=GroupAddress("2049"), payload=GroupValueWrite(DPTArray(DPTTemperature().to_knx(19.85))), source_address=IndividualAddress("1.4.2"), ) cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.4.2")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(xknx, cemi=cemi) knxipframe2 = KNXIPFrame.init_from_body(routing_indication) assert knxipframe2.header.to_knx() == list(raw[0:6]) assert knxipframe2.body.to_knx() == list(raw[6:]) assert knxipframe2.to_knx() == list(raw)
def test_end_to_end_group_write_1byte(self): """Test parsing and streaming CEMIFrame KNX/IP packet, dimm light in my kitchen.""" # Dimm Kitchen L1 to 0x65 raw = bytes.fromhex( "06 10 05 30 00 12 29 00 bc d0 ff f9 01 4b 02 00 80 65") knxipframe = KNXIPFrame() knxipframe.from_knx(raw) telegram = knxipframe.body.cemi.telegram assert telegram == Telegram( destination_address=GroupAddress("331"), payload=GroupValueWrite(DPTArray(0x65)), source_address=IndividualAddress("15.15.249"), ) cemi = CEMIFrame(src_addr=IndividualAddress("15.15.249")) cemi.telegram = telegram cemi.set_hops(5) routing_indication = RoutingIndication(cemi=cemi) knxipframe2 = KNXIPFrame.init_from_body(routing_indication) assert knxipframe2.header.to_knx() == raw[0:6] assert knxipframe2.body.to_knx() == raw[6:] assert knxipframe2.to_knx() == raw
async def handle_cemi_frame(self, cemi: CEMIFrame) -> None: """Handle incoming telegram and send responses if applicable (device management).""" telegram = cemi.telegram telegram.direction = TelegramDirection.INCOMING if response_tgs := await self.telegram_received_callback(telegram): response_code = (CEMIMessageCode.L_DATA_IND if cemi.code is CEMIMessageCode.L_DATA_REQ else CEMIMessageCode.L_DATA_REQ) for response in response_tgs: await self._send_cemi( CEMIFrame.init_from_telegram( telegram=response, code=response_code, src_addr=self._src_address, ))
async def _tunnelling_request(self, telegram: Telegram) -> bool: """Send Telegram to tunnelling device.""" if self.communication_channel is None: raise CommunicationError( "Sending telegram failed. No active communication channel.") cemi = CEMIFrame.init_from_telegram( telegram=telegram, code=CEMIMessageCode.L_DATA_REQ, src_addr=self._src_address, ) tunnelling_request = TunnellingRequest( communication_channel_id=self.communication_channel, sequence_counter=self.sequence_number, cemi=cemi, ) async def _async_wrapper() -> None: self.transport.send(KNXIPFrame.init_from_body(tunnelling_request)) await self._wait_for_tunnelling_request_confirmation( send_tunneling_request_aw=_async_wrapper(), telegram=telegram, ) return True
async def test_tunnelling(self): """Test tunnelling from KNX bus.""" communication_channel_id = 23 data_endpoint = ("192.168.1.2", 4567) udp_transport = UDPTransport(("192.168.1.1", 0), ("192.168.1.2", 1234)) cemi = CEMIFrame.init_from_telegram( Telegram( destination_address=GroupAddress("1/2/3"), payload=GroupValueWrite(DPTArray((0x1, 0x2, 0x3))), ) ) sequence_counter = 42 tunnelling = Tunnelling( udp_transport, data_endpoint, cemi, sequence_counter, communication_channel_id, ) tunnelling.timeout_in_seconds = 0 assert tunnelling.awaited_response_class == TunnellingAck assert tunnelling.communication_channel_id == communication_channel_id # Expected KNX/IP-Frame: tunnelling_request = TunnellingRequest( communication_channel_id=communication_channel_id, sequence_counter=sequence_counter, ) tunnelling_request.cemi = cemi exp_knxipframe = KNXIPFrame.init_from_body(tunnelling_request) with patch("xknx.io.transport.UDPTransport.send") as mock_udp_send, patch( "xknx.io.transport.UDPTransport.getsockname" ) as mock_udp_getsockname: mock_udp_getsockname.return_value = ("192.168.1.3", 4321) await tunnelling.start() mock_udp_send.assert_called_with(exp_knxipframe, addr=data_endpoint) # Response KNX/IP-Frame with wrong type wrong_knxipframe = KNXIPFrame() wrong_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_REQUEST) with patch("logging.Logger.warning") as mock_warning: tunnelling.response_rec_callback(wrong_knxipframe, HPAI(), None) mock_warning.assert_called_with("Could not understand knxipframe") # Response KNX/IP-Frame with error: err_knxipframe = KNXIPFrame() err_knxipframe.init(KNXIPServiceType.TUNNELLING_ACK) err_knxipframe.body.status_code = ErrorCode.E_CONNECTION_ID with patch("logging.Logger.debug") as mock_warning: tunnelling.response_rec_callback(err_knxipframe, HPAI(), None) mock_warning.assert_called_with( "Error: KNX bus responded to request of type '%s' with error in '%s': %s", type(tunnelling).__name__, type(err_knxipframe.body).__name__, ErrorCode.E_CONNECTION_ID, ) # Correct Response KNX/IP-Frame: res_knxipframe = KNXIPFrame() res_knxipframe.init(KNXIPServiceType.TUNNELLING_ACK) tunnelling.response_rec_callback(res_knxipframe, HPAI(), None) assert tunnelling.success
async def test_tunnel_connect_send_disconnect( self, time_travel, route_back, data_endpoint_addr, local_endpoint ): """Test initiating a tunnelling connection.""" local_addr = ("192.168.1.1", 12345) remote_addr = ("192.168.1.2", 3671) self.tunnel.route_back = route_back gateway_data_endpoint = ( HPAI(*data_endpoint_addr) if data_endpoint_addr else HPAI() ) self.tunnel.transport.connect = AsyncMock() self.tunnel.transport.getsockname = Mock(return_value=local_addr) self.tunnel.transport.send = Mock() self.tunnel.transport.stop = Mock() # Connect connect_request = ConnectRequest( control_endpoint=local_endpoint, data_endpoint=local_endpoint, ) connect_frame = KNXIPFrame.init_from_body(connect_request) connection_task = asyncio.create_task(self.tunnel.connect()) await time_travel(0) self.tunnel.transport.connect.assert_called_once() self.tunnel.transport.send.assert_called_once_with(connect_frame) connect_response_frame = KNXIPFrame.init_from_body( ConnectResponse( communication_channel=23, data_endpoint=gateway_data_endpoint, identifier=7, ) ) self.tunnel.transport.handle_knxipframe(connect_response_frame, remote_addr) await connection_task assert self.tunnel._data_endpoint_addr == data_endpoint_addr assert self.tunnel._src_address == IndividualAddress(7) # Send - use data endpoint self.tunnel.transport.send.reset_mock() test_telegram = Telegram(payload=GroupValueWrite(DPTArray((1,)))) test_telegram_frame = KNXIPFrame.init_from_body( TunnellingRequest( communication_channel_id=23, sequence_counter=0, cemi=CEMIFrame.init_from_telegram( test_telegram, code=CEMIMessageCode.L_DATA_REQ, src_addr=IndividualAddress(7), ), ) ) asyncio.create_task(self.tunnel.send_telegram(test_telegram)) await time_travel(0) self.tunnel.transport.send.assert_called_once_with( test_telegram_frame, addr=data_endpoint_addr ) # skip ack and confirmation # Disconnect self.tunnel.transport.send.reset_mock() disconnect_request = DisconnectRequest( communication_channel_id=23, control_endpoint=local_endpoint, ) disconnect_frame = KNXIPFrame.init_from_body(disconnect_request) disconnection_task = asyncio.create_task(self.tunnel.disconnect()) await time_travel(0) self.tunnel.transport.send.assert_called_once_with(disconnect_frame) disconnect_response_frame = KNXIPFrame.init_from_body( DisconnectResponse(communication_channel_id=23) ) self.tunnel.transport.handle_knxipframe(disconnect_response_frame, remote_addr) await disconnection_task assert self.tunnel._data_endpoint_addr is None self.tunnel.transport.stop.assert_called_once()