def load_yaml(fname: str) -> JSON_TYPE: """Load a YAML file.""" try: with open(fname, encoding="utf-8") as conf_file: # If configuration file is empty YAML returns None # We convert that to an empty dict return yaml.load(conf_file, Loader=SafeLineLoader) or OrderedDict() except yaml.YAMLError as exc: logger.error(str(exc)) raise XKNXException(exc) from exc except UnicodeDecodeError as exc: logger.error("Unable to read file %s: %s", fname, exc) raise XKNXException(exc) from exc
def test_sync_exception(self): """Testing exception handling within sync().""" # pylint: disable=protected-access xknx = XKNX(loop=self.loop) device = Device(xknx, 'TestDevice') with patch('logging.Logger.error') as mock_error: with patch('xknx.devices.Device._sync_impl') as mock_sync_impl: fut = asyncio.Future() fut.set_result(None) mock_sync_impl.return_value = fut mock_sync_impl.side_effect = XKNXException() self.loop.run_until_complete(asyncio.Task(device.sync())) mock_sync_impl.assert_called_with(True) mock_error.assert_called_with('Error while syncing device: %s', XKNXException())
def send_telegram(self, telegram): """ Send Telegram to routing tunelling 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 = yield from self._send_telegram_impl(telegram) if not success: self.xknx.logger.warning( "Sending of telegram failed. Retrying a second time.") success = yield from self._send_telegram_impl(telegram) if not success: self.xknx.logger.warning( "Resending telegram failed. Reconnecting to tunnel.") yield from self.reconnect() success = yield from self._send_telegram_impl(telegram) if not success: raise XKNXException("Could not send telegram to tunnel") self.increase_sequence_number()
def test_config_file_error(self): """Test error message when reading an errornous config file.""" with patch("logging.Logger.error") as mock_err, patch( "xknx.config.ConfigV1.parse_group_light") as mock_parse: mock_parse.side_effect = XKNXException() XKNX(config="xknx.yaml") self.assertEqual(mock_err.call_count, 1)
def validate_ip(address: str, address_name: str = "IP address") -> None: """Raise an exception if address cannot be parsed as IPv4 address.""" try: ipaddress.IPv4Address(address) except ipaddress.AddressValueError as ex: raise XKNXException("%s is not a valid IPv4 address." % address_name) from ex
def test_config_file_error(self): """Test error message when reading an errornous config file.""" with patch('logging.Logger.error') as mock_err, \ patch('xknx.core.Config.parse_group_light') as mock_parse: mock_parse.side_effect = XKNXException() XKNX(config='xknx.yaml', loop=self.loop) self.assertEqual(mock_err.call_count, 1)
async def start_automatic(self, scan_filter: GatewayScanFilter): """Start GatewayScanner and connect to the found device.""" gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter) gateways = await gatewayscanner.scan() if not gateways: raise XKNXException("No Gateways found") gateway = gateways[0] # on Linux gateway.local_ip can be any interface listening to the # multicast group (even 127.0.0.1) so we set the interface with find_local_ip local_interface_ip = self.find_local_ip(gateway_ip=gateway.ip_addr) if gateway.supports_tunnelling and scan_filter.routing is not True: await self.start_tunnelling( local_interface_ip, self.connection_config.local_port, gateway.ip_addr, gateway.port, self.connection_config.auto_reconnect, self.connection_config.auto_reconnect_wait, bind_ip=self.connection_config.bind_ip, bind_port=self.connection_config.bind_port, ) elif gateway.supports_routing: await self.start_routing(local_interface_ip)
def send(self, knxipframe): """Send KNXIPFrame to socket.""" self.xknx.knx_logger.debug("Sending: %s", knxipframe) if self.transport is None: raise XKNXException("Transport not connected") if self.multicast: self.transport.sendto(bytes(knxipframe.to_knx()), self.remote_addr) else: self.transport.sendto(bytes(knxipframe.to_knx()))
async def disconnect(self, ignore_error=False): """Disconnect from tunnel device.""" disconnect = Disconnect( self.xknx, self.udp_client, communication_channel_id=self.communication_channel) await disconnect.start() if not disconnect.success and not ignore_error: raise XKNXException("Could not disconnect channel") else: self.xknx.logger.debug("Tunnel disconnected (communication_channel: %s)", self.communication_channel)
async def _start_routing(self, local_ip: str | None = None) -> None: """Start KNX/IP Routing.""" local_ip = local_ip or await util.get_default_local_ip() if local_ip is None: raise XKNXException("No network interface found.") util.validate_ip(local_ip, address_name="Local IP address") logger.debug("Starting Routing from %s as %s", local_ip, self.xknx.own_address) self._interface = Routing(self.xknx, self.telegram_received, local_ip) await self._interface.connect()
def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> str: """Load environment variables and embed it into the configuration YAML.""" args = node.value.split() # Check for a default value if len(args) > 1: return os.getenv(args[0], " ".join(args[1:])) if args[0] in os.environ: return os.environ[args[0]] logger.error("Environment variable %s not defined", node.value) raise XKNXException(node.value)
def connect(self): """Connect/build tunnel.""" connect = Connect(self.xknx, self.udp_client) yield from connect.start() if not connect.success: raise XKNXException("Could not establish connection") self.xknx.logger.debug( "Tunnel established communication_channel=%s, id=%s", connect.communication_channel, connect.identifier) self.communication_channel = connect.communication_channel self.sequence_number = 0 yield from self.start_heartbeat()
def _include_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> JSON_TYPE: """Load another YAML file and embeds it using the !include tag. Example: device_tracker: !include device_tracker.yaml """ fname = os.path.join(os.path.dirname(loader.name), node.value) try: return _add_reference(load_yaml(fname), loader, node) except FileNotFoundError as exc: raise XKNXException(f"{node.start_mark}: Unable to read file {fname}.") from exc
def start_automatic(self): """Start GatewayScanner and connect to the found device.""" gatewayscanner = GatewayScanner(self.xknx) yield from gatewayscanner.start() gatewayscanner.stop() if not gatewayscanner.found: raise XKNXException("No Gateways found") if gatewayscanner.supports_tunneling: yield from self.start_tunnelling(gatewayscanner.found_local_ip, gatewayscanner.found_ip_addr, gatewayscanner.found_port) elif gatewayscanner.supports_routing: yield from self.start_routing(gatewayscanner.found_local_ip)
async def start_automatic(self, scan_filter: GatewayScanFilter): """Start GatewayScanner and connect to the found device.""" gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter) gateways = await gatewayscanner.scan() if not gateways: raise XKNXException("No Gateways found") gateway = gateways[0] if gateway.supports_tunnelling: await self.start_tunnelling(gateway.local_ip, gateway.ip_addr, gateway.port) elif gateway.supports_routing: bind_to_multicast_addr = get_os_name() != "Darwin" # = Mac OS await self.start_routing(gateway.local_ip, bind_to_multicast_addr)
async def disconnect(self, ignore_error=False): """Disconnect from tunnel device.""" # only send disconnect request if we ever were connected if self.communication_channel is None: # close udp client to prevent open file descriptors await self.udp_client.stop() return disconnect = Disconnect( self.xknx, self.udp_client, communication_channel_id=self.communication_channel) await disconnect.start() if not disconnect.success and not ignore_error: raise XKNXException("Could not disconnect channel") self.xknx.logger.debug("Tunnel disconnected (communication_channel: %s)", self.communication_channel) # close udp client to prevent open file descriptors await self.udp_client.stop()
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( self.xknx, self.udp_client, communication_channel_id=self.communication_channel, route_back=self.route_back, ) await disconnect.start() if not disconnect.success and not ignore_error: self.communication_channel = None raise XKNXException("Could not disconnect channel") logger.debug( "Tunnel disconnected (communication_channel: %s)", self.communication_channel, ) self.communication_channel = None
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: msg = "Cannot connect to KNX. Retry in {} seconds.".format( self.auto_reconnect_wait) self.xknx.logger.warning(msg) task = self.xknx.loop.create_task(self.schedule_reconnect()) self._reconnect_task = task return raise XKNXException("Could not establish connection") self.xknx.logger.debug( "Tunnel established communication_channel=%s, id=%s", connect.communication_channel, connect.identifier) self._reconnect_task = None self.communication_channel = connect.communication_channel self.sequence_number = 0 await self.start_heartbeat()
async def _scan( self, queue: asyncio.Queue[GatewayDescriptor | None] | None = None ) -> None: """Scan for gateways.""" local_ip = self.local_ip or await util.get_default_local_ip( remote_ip=self.xknx.multicast_group) if local_ip is None: if queue is not None: queue.put_nowait(None) raise XKNXException("No usable network interface found.") interface_name = util.get_local_interface_name(local_ip=local_ip) logger.debug("Searching on %s / %s", interface_name, local_ip) udp_transport = UDPTransport( local_addr=(local_ip, 0), remote_addr=(self.xknx.multicast_group, self.xknx.multicast_port), ) udp_transport.register_callback( partial(self._response_rec_callback, interface=interface_name, queue=queue), [ KNXIPServiceType.SEARCH_RESPONSE, KNXIPServiceType.SEARCH_RESPONSE_EXTENDED, ], ) try: await self._send_search_requests(udp_transport=udp_transport) await asyncio.wait_for( self._response_received_event.wait(), timeout=self.timeout_in_seconds, ) except asyncio.TimeoutError: pass except asyncio.CancelledError: pass finally: udp_transport.stop() if queue is not None: queue.put_nowait(None)
async def _start_tunnelling_udp( self, gateway_ip: str, gateway_port: int, ) -> None: """Start KNX/IP UDP tunnel.""" util.validate_ip(gateway_ip, address_name="Gateway IP address") local_ip = self.connection_config.local_ip or util.find_local_ip( gateway_ip=gateway_ip) local_port = self.connection_config.local_port route_back = self.connection_config.route_back if local_ip is None: local_ip = await util.get_default_local_ip(gateway_ip) if local_ip is None: raise XKNXException("No network interface found.") route_back = True logger.debug( "Falling back to default interface and enabling route back.") util.validate_ip(local_ip, address_name="Local IP address") logger.debug( "Starting tunnel from %s:%s to %s:%s", local_ip, local_port, gateway_ip, gateway_port, ) self._interface = UDPTunnel( self.xknx, gateway_ip=gateway_ip, gateway_port=gateway_port, local_ip=local_ip, local_port=local_port, route_back=route_back, telegram_received_callback=self.telegram_received, auto_reconnect=self.connection_config.auto_reconnect, auto_reconnect_wait=self.connection_config.auto_reconnect_wait, ) await self._interface.connect()
def parse_connection(self, doc): """Parse the connection section of xknx.yaml.""" if "connection" in doc \ and hasattr(doc["connection"], '__iter__'): for conn, prefs in doc["connection"].items(): try: if conn == "tunneling": if prefs is None or \ "gateway_ip" not in prefs: raise XKNXException( "`gateway_ip` is required for tunneling connection." ) conn_type = ConnectionType.TUNNELING elif conn == "routing": conn_type = ConnectionType.ROUTING else: conn_type = ConnectionType.AUTOMATIC self._parse_connection_prefs(conn_type, prefs) except XKNXException as ex: self.xknx.logger.error( "Error while reading config file: Could not parse %s: %s", conn, ex) raise ex
( CouldNotParseKNXIP("desc1"), CouldNotParseKNXIP("desc1"), CouldNotParseKNXIP("desc2"), ), ( CouldNotParseTelegram("desc", arg1=1, arg2=2), CouldNotParseTelegram("desc", arg1=1, arg2=2), CouldNotParseTelegram("desc", arg1=2, arg2=1), ), ( DeviceIllegalValue("value1", "desc"), DeviceIllegalValue("value1", "desc"), DeviceIllegalValue("value1", "desc2"), ), ( XKNXException("desc1"), XKNXException("desc1"), XKNXException("desc2"), ), ], ) def test_exceptions(base, equal, diff): """Test hashability and repr of exceptions.""" assert hash(base) == hash(equal) assert hash(base) != hash(diff) assert base == equal assert base != diff assert repr(base) == repr(equal) assert repr(base) != repr(diff)