Example #1
0
    async def send_telegram(self, telegram: Telegram) -> None:
        """Send telegram to connected device (either Tunneling or Routing)."""
        if self._interface is None:
            raise CommunicationError("KNX/IP interface not connected")

        return await self._await_from_different_thread(
            self._interface.send_telegram(telegram), self._thread_loop)
Example #2
0
 async def connect(self) -> bool:
     """Connect to a KNX tunneling interface. Returns True on success."""
     await self.xknx.connection_manager.connection_state_changed(
         XknxConnectionState.CONNECTING)
     try:
         await self.transport.connect()
         await self.setup_tunnel()
         await self._connect_request()
     except (OSError, CommunicationError) as ex:
         logger.debug(
             "Could not establish connection to KNX/IP interface. %s: %s",
             type(ex).__name__,
             ex,
         )
         await self.xknx.connection_manager.connection_state_changed(
             XknxConnectionState.DISCONNECTED)
         if not self._initial_connection and self.auto_reconnect:
             self._reconnect_task = asyncio.create_task(self._reconnect())
             return False
         # close transport to prevent open file descriptors
         self.transport.stop()
         raise CommunicationError(
             "Tunnel connection could not be established") from ex
     else:
         self._tunnel_established()
         await self.xknx.connection_manager.connection_state_changed(
             XknxConnectionState.CONNECTED)
         return True
Example #3
0
    async def send_telegram(self, telegram: Telegram) -> None:
        """
        Send Telegram to routing tunnelling device - retry mechanism.

        If a TUNNELLING_REQUEST frame is not confirmed within the TUNNELLING_REQUEST_TIME_- OUT
        time of one (1) second then the frame shall be repeated once with the same sequence counter
        value by the sending KNXnet/IP device.

        If the KNXnet/IP device does not receive a TUNNELLING_ACK frame within the
        TUNNELLING_- REQUEST_TIMEOUT (= 1 second) or the status of a received
        TUNNELLING_ACK frame signals any kind of error condition, the sending device
        shall repeat the TUNNELLING_REQUEST frame once and then terminate the
        connection by sending a DISCONNECT_REQUEST frame to the other device’s
        control endpoint.
        """
        success = await self._tunnelling_request(telegram)
        if not success:
            logger.debug("Sending of telegram failed. Retrying a second time.")
            success = await self._tunnelling_request(telegram)
            if not success:
                logger.debug(
                    "Resending telegram failed. Reconnecting to tunnel.")
                # TODO: How to test this?
                if self._reconnect_task is None or self._reconnect_task.done():
                    self._tunnel_lost()
                await self.xknx.connected.wait()
                success = await self._tunnelling_request(telegram)
                if not success:
                    raise CommunicationError(
                        "Resending the telegram repeatedly failed.", True)
        self._increase_sequence_number()
Example #4
0
File: tunnel.py Project: XKNX/xknx
    async def _tunnelling_request(self, cemi: CEMIFrame) -> bool:
        """Send Telegram to tunnelling device."""
        if self.communication_channel is None:
            raise CommunicationError(
                "Sending telegram failed. No active communication channel.")
        tunnelling_request = TunnellingRequest(
            communication_channel_id=self.communication_channel,
            sequence_counter=self.sequence_number,
            cemi=cemi,
        )

        if cemi.code is CEMIMessageCode.L_DATA_REQ:

            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(),
                cemi=cemi,
            )
        else:
            self.transport.send(KNXIPFrame.init_from_body(tunnelling_request))

        return True
Example #5
0
 async def _start_automatic(self) -> None:
     """Start GatewayScanner and connect to the found device."""
     async for gateway in GatewayScanner(
             self.xknx,
             local_ip=self.connection_config.local_ip,
             scan_filter=self.connection_config.
             scan_filter,  # secure disabled by default
     ).async_scan():
         try:
             if gateway.supports_tunnelling_tcp:
                 await self._start_tunnelling_tcp(
                     gateway_ip=gateway.ip_addr,
                     gateway_port=gateway.port,
                 )
             elif gateway.supports_tunnelling:
                 await self._start_tunnelling_udp(
                     gateway_ip=gateway.ip_addr,
                     gateway_port=gateway.port,
                 )
             elif gateway.supports_routing:
                 await self._start_routing(
                     local_ip=self.connection_config.local_ip, )
         except CommunicationError as ex:
             logger.debug("Could not connect to %s: %s", gateway, ex)
             continue
         else:
             self._gateway_info = gateway
             break
     else:
         raise CommunicationError("No usable KNX/IP device found.")
Example #6
0
 async def connect(self):
     """Connect/build tunnel."""
     connect = Connect(self.xknx, self.udp_client)
     await connect.start()
     if not connect.success:
         if self.auto_reconnect:
             logger.warning(
                 "Could not connect to KNX. Retry in %s seconds.",
                 self.auto_reconnect_wait,
             )
             self._reconnect_task = asyncio.create_task(self.schedule_reconnect())
             return
         raise CommunicationError(
             "Could not establish connection", not self._is_reconnecting
         )
     logger.debug(
         "Tunnel established communication_channel=%s, id=%s",
         connect.communication_channel,
         connect.identifier,
     )
     if self._is_reconnecting:
         logger.info("Successfully reconnected to KNX bus.")
     self._reconnect_task = None
     self._is_reconnecting = False
     self.communication_channel = connect.communication_channel
     # Use the individual address provided by the tunnelling server
     self._src_address = PhysicalAddress(connect.identifier)
     self.sequence_number = 0
     await self.start_heartbeat()
Example #7
0
 async def _do_heartbeat_failed(self) -> None:
     """Heartbeat: handling error."""
     # first heartbeat failed - try 3 more times before disconnecting.
     for _heartbeats_failed in range(3):
         if await self._connectionstate_request():
             return
     # 3 retries failed
     raise CommunicationError("No answer from tunneling server.")
Example #8
0
 def _tunnel_lost(self) -> None:
     """Prepare for reconnection or shutdown when the connection is lost. Callback."""
     self.xknx.connected.clear()
     self.stop_heartbeat()
     if self.auto_reconnect:
         self._reconnect_task = asyncio.create_task(self._reconnect())
     else:
         raise CommunicationError("Tunnel connection closed.")
Example #9
0
 async def process_telegram_outgoing(self, telegram: Telegram) -> None:
     """Process outgoing telegram."""
     telegram_logger.debug(telegram)
     if self.xknx.knxip_interface is not None:
         await self.xknx.knxip_interface.send_telegram(telegram)
         if isinstance(telegram.payload, GroupValueWrite):
             await self.xknx.devices.process(telegram)
     else:
         raise CommunicationError("No KNXIP interface defined")
Example #10
0
 def _tunnel_lost(self) -> None:
     """Prepare for reconnection or shutdown when the connection is lost. Callback."""
     asyncio.create_task(
         self.xknx.connection_manager.connection_state_changed(
             XknxConnectionState.DISCONNECTED))
     self.stop_heartbeat()
     if self.auto_reconnect:
         self._reconnect_task = asyncio.create_task(self._reconnect())
     else:
         raise CommunicationError("Tunnel connection closed.")
Example #11
0
    async def process_telegram_outgoing(self, telegram: Telegram) -> None:
        """Process outgoing telegram."""
        telegram_logger.debug(telegram)
        if not isinstance(telegram.destination_address, InternalGroupAddress):
            if self.xknx.knxip_interface is None:
                raise CommunicationError("No KNXIP interface defined")
            await self.xknx.knxip_interface.send_telegram(telegram)

        await self.xknx.devices.process(telegram)
        await self._run_telegram_received_cbs(telegram)
Example #12
0
 async def _connectionstate_request(self) -> bool:
     """Return state of tunnel. True if tunnel is in good shape."""
     if self.communication_channel is None:
         raise CommunicationError("No active communication channel.")
     conn_state = ConnectionState(
         transport=self.transport,
         communication_channel_id=self.communication_channel,
         local_hpai=self.local_hpai,
     )
     await conn_state.start()
     return conn_state.success
Example #13
0
 async def _connectionstate_request(self) -> bool:
     """Return state of tunnel. True if tunnel is in good shape."""
     if self.communication_channel is None:
         raise CommunicationError("No active communication channel.")
     conn_state = ConnectionState(
         self.xknx,
         self.udp_client,
         communication_channel_id=self.communication_channel,
         route_back=self.route_back,
     )
     await conn_state.start()
     return conn_state.success
Example #14
0
    async def process_telegram_outgoing(self, telegram: Telegram) -> None:
        """Process outgoing telegram."""
        telegram_logger.debug(telegram)
        if self.xknx.knxip_interface is not None:
            await self.xknx.knxip_interface.send_telegram(telegram)
            if isinstance(telegram.payload, GroupValueWrite):
                await self.xknx.devices.process(telegram)

            for telegram_received_cb in self.telegram_received_cbs:
                if telegram_received_cb.is_within_filter(telegram):
                    await telegram_received_cb.callback(telegram)
        else:
            raise CommunicationError("No KNXIP interface defined")
Example #15
0
    def send(self,
             knxipframe: KNXIPFrame,
             addr: tuple[str, int] | None = None) -> None:
        """Send KNXIPFrame to socket. `addr` is ignored on TCP."""
        knx_logger.debug(
            "Sending to %s at %s:\n%s",
            self.remote_hpai,
            time.time(),
            knxipframe,
        )
        if self.transport is None:
            raise CommunicationError("Transport not connected")

        self.transport.write(knxipframe.to_knx())
Example #16
0
    async def connect(self) -> None:
        """Connect transport."""
        await super().connect()
        self._private_key, self.public_key = generate_ecdh_key_pair()
        self._sequence_number = 0
        self._sequence_number_received = -1
        request_session = Session(
            transport=self,
            ecdh_client_public_key=self.public_key,
        )
        await request_session.start()
        if request_session.response is None:
            raise CommunicationError(
                "Secure session could not be established. No response received."
            )
        # SessionAuthenticate and everything else after now shall be wrapped in SecureWrapper
        authenticate_mac = self.handshake(request_session.response)
        self.initialized = True

        request_authentication = Authenticate(
            transport=self,
            user_id=self.user_id,
            message_authentication_code=authenticate_mac,
        )
        await request_authentication.start()
        if request_authentication.response is None:
            raise CommunicationError(
                "Secure session could not be established. No response received."
            )
        if (  # TODO: look for status in request/response and use `success` instead of response ?
                request_authentication.response.status !=
                SecureSessionStatusCode.STATUS_AUTHENTICATION_SUCCESS):
            raise CommunicationError(
                f"Secure session authentication failed: {request_authentication.response.status}"
            )
        self._session_status_handler = self.register_callback(
            self._handle_session_status, [KNXIPServiceType.SESSION_STATUS])
Example #17
0
async def test_failed_connect_disconnect(_if_mock):
    """Test failing connections."""
    xknx = XKNX()
    ia_1 = IndividualAddress("4.0.1")

    xknx.knxip_interface.send_telegram.side_effect = ConfirmationError("")
    with pytest.raises(ManagementConnectionError):
        await xknx.management.connect(ia_1)

    xknx.knxip_interface.send_telegram.side_effect = CommunicationError("")
    with pytest.raises(ManagementConnectionError):
        await xknx.management.connect(ia_1)

    xknx.knxip_interface.send_telegram.side_effect = None
    conn_1 = await xknx.management.connect(ia_1)
    xknx.knxip_interface.send_telegram.side_effect = ConfirmationError("")
    with pytest.raises(ManagementConnectionError):
        await xknx.management.disconnect(ia_1)

    xknx.knxip_interface.send_telegram.side_effect = None
    conn_1 = await xknx.management.connect(ia_1)
    xknx.knxip_interface.send_telegram.side_effect = CommunicationError("")
    with pytest.raises(ManagementConnectionError):
        await conn_1.disconnect()
Example #18
0
 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.")
     tunnelling = Tunnelling(
         self.xknx,
         self.udp_client,
         telegram,
         self._src_address,
         self.sequence_number,
         self.communication_channel,
     )
     await tunnelling.start()
     return tunnelling.success
Example #19
0
    def handshake(self, session_response: SessionResponse) -> bytes:
        """
        Handshake with device.

        Returns a SessionAuthenticate KNX/IP body.
        """
        self._peer_public_key = X25519PublicKey.from_public_bytes(
            session_response.ecdh_server_public_key)
        self.session_id = session_response.secure_session_id
        # verify SessionResponse MAC
        # TODO: get header data from actual KNX/IP frame
        response_header_data = bytes.fromhex("06 10 09 52 00 38")
        pub_keys_xor = bytes_xor(
            self.public_key,
            session_response.ecdh_server_public_key,
        )
        if self._device_authentication_code:
            response_mac_cbc = calculate_message_authentication_code_cbc(
                key=self._device_authentication_code,
                additional_data=response_header_data +
                self.session_id.to_bytes(2, "big") +
                pub_keys_xor,  # knx_ip_header + secure_session_id + bytes_xor(client_pub_key, server_pub_key)
            )
            _, mac_tr = decrypt_ctr(
                key=self._device_authentication_code,
                counter_0=COUNTER_0_HANDSHAKE,
                mac=session_response.message_authentication_code,
            )
            if mac_tr != response_mac_cbc:
                raise CommunicationError(
                    "SessionResponse MAC verification failed.")
        # calculate session key
        ecdh_shared_secret = self._private_key.exchange(self._peer_public_key)
        self._session_key = sha256_hash(ecdh_shared_secret)[:16]
        # generate SessionAuthenticate MAC
        authenticate_header_data = bytes.fromhex("06 10 09 53 00 18")
        authenticate_mac_cbc = calculate_message_authentication_code_cbc(
            key=self._user_password,
            additional_data=authenticate_header_data + bytes(1)  # reserved
            + self.user_id.to_bytes(1, "big") + pub_keys_xor,
            block_0=bytes(16),
        )
        _, authenticate_mac = encrypt_data_ctr(
            key=self._user_password,
            counter_0=COUNTER_0_HANDSHAKE,
            mac_cbc=authenticate_mac_cbc,
        )
        return authenticate_mac
Example #20
0
 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.")
     tunnelling = Tunnelling(
         transport=self.transport,
         data_endpoint=self._data_endpoint_addr,
         telegram=telegram,
         src_address=self._src_address,
         sequence_counter=self.sequence_number,
         communication_channel_id=self.communication_channel,
     )
     await self._wait_for_tunnelling_request_confirmation(
         send_tunneling_request_aw=tunnelling.start(), telegram=telegram)
     return tunnelling.success
Example #21
0
 async def _disconnect_request(self, ignore_error: bool = False) -> None:
     """Disconnect from tunnel device. Delete communication_channel."""
     if self.communication_channel is not None:
         disconnect = Disconnect(
             transport=self.transport,
             communication_channel_id=self.communication_channel,
             local_hpai=self.local_hpai,
         )
         await disconnect.start()
         if not disconnect.success and not ignore_error:
             self.communication_channel = None
             raise CommunicationError("Could not disconnect channel")
         logger.debug(
             "Tunnel disconnected (communication_channel: %s)",
             self.communication_channel,
         )
     self.communication_channel = None
Example #22
0
File: tunnel.py Project: XKNX/xknx
 async def _tunnelling_request(self, cemi: CEMIFrame) -> bool:
     """Send Telegram to tunnelling device."""
     if self.communication_channel is None:
         raise CommunicationError(
             "Sending telegram failed. No active communication channel.")
     tunnelling = Tunnelling(
         transport=self.transport,
         data_endpoint=self._data_endpoint_addr,
         cemi=cemi,
         sequence_counter=self.sequence_number,
         communication_channel_id=self.communication_channel,
     )
     if cemi.code is CEMIMessageCode.L_DATA_REQ:
         await self._wait_for_tunnelling_request_confirmation(
             send_tunneling_request_aw=tunnelling.start(), cemi=cemi)
     else:
         await tunnelling.start()
     return tunnelling.success
Example #23
0
    def send(self, knxipframe: KNXIPFrame, addr: tuple[str, int] | None = None) -> None:
        """Send KNXIPFrame to socket."""
        _addr = addr or self.remote_addr
        knx_logger.debug(
            "Sending to %s:%s at %s:\n %s", _addr[0], _addr[1], time.time(), knxipframe
        )
        if self.transport is None:
            raise CommunicationError("Transport not connected")

        if self.multicast:
            if addr is not None:
                logger.warning(
                    "Multicast send to specific address is invalid. %s",
                    knxipframe,
                )
            self.transport.sendto(knxipframe.to_knx(), self.remote_addr)
        else:
            self.transport.sendto(knxipframe.to_knx(), addr=_addr)
Example #24
0
 async def _connect_request(self) -> bool:
     """Connect to tunnelling server. Set communication_channel and src_address."""
     connect = Connect(self.xknx,
                       self.udp_client,
                       route_back=self.route_back)
     await connect.start()
     if connect.success:
         self.communication_channel = connect.communication_channel
         # Use the individual address provided by the tunnelling server
         self._src_address = IndividualAddress(connect.identifier)
         logger.debug(
             "Tunnel established communication_channel=%s, id=%s",
             connect.communication_channel,
             connect.identifier,
         )
         return True
     raise CommunicationError(
         f"ConnectRequest failed. Status code: {connect.response_status_code}"
     )
Example #25
0
 async def connect(self) -> bool:
     """Start routing."""
     await self.xknx.connection_manager.connection_state_changed(
         XknxConnectionState.CONNECTING)
     try:
         await self.udp_transport.connect()
     except OSError as ex:
         logger.debug(
             "Could not establish connection to KNX/IP network. %s: %s",
             type(ex).__name__,
             ex,
         )
         await self.xknx.connection_manager.connection_state_changed(
             XknxConnectionState.DISCONNECTED)
         # close udp transport to prevent open file descriptors
         self.udp_transport.stop()
         raise CommunicationError("Routing could not be started") from ex
     await self.xknx.connection_manager.connection_state_changed(
         XknxConnectionState.CONNECTED)
     return True
Example #26
0
 async def _connect_request(self) -> bool:
     """Connect to tunnelling server. Set communication_channel and src_address."""
     connect = Connect(transport=self.transport, local_hpai=self.local_hpai)
     await connect.start()
     if connect.success:
         self.communication_channel = connect.communication_channel
         # assign data_endpoint received from server
         self._data_endpoint_addr = (connect.data_endpoint.addr_tuple
                                     if not connect.data_endpoint.route_back
                                     else None)
         # Use the individual address provided by the tunnelling server
         self._src_address = IndividualAddress(connect.identifier)
         self.xknx.current_address = self._src_address
         logger.debug(
             "Tunnel established communication_channel=%s, address=%s",
             connect.communication_channel,
             self._src_address,
         )
         return True
     raise CommunicationError(
         f"ConnectRequest failed. Status code: {connect.response_status_code}"
     )
Example #27
0
File: tunnel.py Project: XKNX/xknx
    async def _send_cemi(self, cemi: CEMIFrame) -> None:
        """
        Send CEMI Frame to tunnelling server - retry mechanism.

        A transport layer confirmation shall be awaited before sending the next telegram.

        If a TUNNELLING_REQUEST frame is not confirmed within the TUNNELLING_REQUEST_TIMEOUT
        time of one (1) second then the frame shall be repeated once with the same sequence counter
        value by the sending KNXnet/IP device.

        If the KNXnet/IP device does not receive a TUNNELLING_ACK frame within the
        TUNNELLING_REQUEST_TIMEOUT (= 1 second) or the status of a received
        TUNNELLING_ACK frame signals any kind of error condition, the sending device
        shall repeat the TUNNELLING_REQUEST frame once and then terminate the
        connection by sending a DISCONNECT_REQUEST frame to the other devices
        control endpoint.
        """
        async with self._send_telegram_lock:
            try:
                if await self._tunnelling_request(cemi):
                    return

                logger.debug(
                    "Sending of telegram failed. Retrying a second time.")
                if await self._tunnelling_request(cemi):
                    return

                logger.debug(
                    "Resending telegram failed. Reconnecting to tunnel.")
                if self._reconnect_task is None or self._reconnect_task.done():
                    self._tunnel_lost()
                await self.xknx.connection_manager.connected.wait()
                if not await self._tunnelling_request(cemi):
                    raise CommunicationError(
                        "Resending the telegram repeatedly failed.", True)
            finally:
                self._increase_sequence_number()
Example #28
0
 async def send_telegram(self, telegram: "Telegram") -> None:
     """Send telegram to connected device (either Tunneling or Routing)."""
     if self.interface is not None:
         await self.interface.send_telegram(telegram)
     else:
         raise CommunicationError("KNX/IP interface not connected")
Example #29
0
 def getsockname(self) -> Tuple[str, int]:
     """Return socket IP and port."""
     if self.transport is None:
         raise CommunicationError(
             "No transport defined. Socket information not resolveable")
     return cast(Tuple[str, int], self.transport.get_extra_info("sockname"))