def conn_opc(): # OPCサーバに接続 cl = Client("opc.tcp://192.168.10.5:51110/CogentDataHub/DataAccess") # クライアント証明書のapplication_uri cl.application_uri = "urn:desktop-i50i89m:Cogent DataHub" # policy設定 print("secPolicy: " + str(secPolicy)) if secPolicy != policies[0]: # None以外の場合SecurityPolicyを設定 mode = ua.MessageSecurityMode.SignAndEncrypt pc = getattr(security_policies, 'SecurityPolicy' + secPolicy) # 第二引数:クライアント証明書 cl.set_security( pc, "/Users/watarium/PycharmProjects/opcua/OPCUA_CL.der", "/Users/watarium/PycharmProjects/opcua/OPCUAClient.pem", "/Users/watarium/PycharmProjects/opcua/OPCUAServer.der", mode) # 認証設定 if setCert == certs[1]: # user/pass cl.set_user("admin") cl.set_password("1234") elif setCert == certs[2]: # certificate cl.load_private_key( "/Users/watarium/PycharmProjects/opcua/OPCUAClient.pem") cl.load_client_certificate( "/Users/watarium/PycharmProjects/opcua/OPCUA_CL.der") try: # 接続 print("Policy: {0}, Certificate: {1}".format(secPolicy, setCert)) print("---------------------Connection start-----------------------") cl.connect() sleep(5) # 情報取得 ep = cl.get_endpoints() print(ep[0].Server.ApplicationUri) root = cl.get_root_node() print("Objects node is: ", root) print(cl.get_namespace_array()) print(cl.get_namespace_index('urn:win-9hi38ajrojd:Cogent DataHub')) #これがうまくいかなかった(2019/06/27) #node = cl.get_node('ns=1;s=xxxxx') #print(node.get_value()) #node.set_attribute(ua.AttributeIds.Value, 1) # 切断 cl.disconnect() print("-------------------Connection Success!!!--------------------") except Exception as e: print("---------------------Connection Faild!!---------------------") print(e) cl.disconnect()
from opcua import Client path = "opc.tcp://192.168.10.141:49320/OPCUA/server/" # bare endre IP. Sjekk "OPC UA Configuration TAB i kepware(taskbar) og aktiver URL. Port skal være riktig client = Client(path) client.connect() print(client.get_namespace_array()) print("Server node = " + str(client.get_server_node()) + "\n") objects = client.get_objects_node() print("Objects: " + str(objects)) for o in objects.get_children(): print("Children of objects are: ", o) root = client.get_root_node() print("Root node is: ", root) print("childs of root are: ", root.get_children()) print("name of root is", root.get_browse_name()) var = objects.get_child(["3:Channel1.Device1", "tag1"]) print(var) print("Value of variable is: ", str(var.get_value())) def get_children_names(_children): for i in _children: _val = str(i) int(_val.index("=")) _index = [pos for pos, char in enumerate(_val) if char == "="] _index.reverse() _outval = _val[_index[0] + 1:len(_val)] print(_outval)
class OpcUaConnector(Thread, Connector): def __init__(self, gateway, config, connector_type): self._connector_type = connector_type self.statistics = {'MessagesReceived': 0, 'MessagesSent': 0} super().__init__() self.__gateway = gateway self.__server_conf = config.get("server") self.__interest_nodes = [] self.__available_object_resources = {} self.__show_map = self.__server_conf.get("showMap", False) self.__previous_scan_time = 0 for mapping in self.__server_conf["mapping"]: if mapping.get("deviceNodePattern") is not None: self.__interest_nodes.append( {mapping["deviceNodePattern"]: mapping}) else: log.error( "deviceNodePattern in mapping: %s - not found, add property deviceNodePattern to processing this mapping", dumps(mapping)) if "opc.tcp" not in self.__server_conf.get("url"): opcua_url = "opc.tcp://" + self.__server_conf.get("url") else: opcua_url = self.__server_conf.get("url") self.client = Client( opcua_url, timeout=self.__server_conf.get("timeoutInMillis", 4000) / 1000) if self.__server_conf["identity"]["type"] == "cert.PEM": try: ca_cert = self.__server_conf["identity"].get("caCert") private_key = self.__server_conf["identity"].get("privateKey") cert = self.__server_conf["identity"].get("cert") security_mode = self.__server_conf["identity"].get( "mode", "SignAndEncrypt") policy = self.__server_conf["security"] if cert is None or private_key is None: log.exception( "Error in ssl configuration - cert or privateKey parameter not found" ) raise security_string = policy + ',' + security_mode + ',' + cert + ',' + private_key if ca_cert is not None: security_string = security_string + ',' + ca_cert self.client.set_security_string(security_string) except Exception as e: log.exception(e) if self.__server_conf["identity"].get("username"): self.client.set_user( self.__server_conf["identity"].get("username")) if self.__server_conf["identity"].get("password"): self.client.set_password( self.__server_conf["identity"].get("password")) self.setName( self.__server_conf.get( "name", 'OPC-UA ' + ''.join(choice(ascii_lowercase) for _ in range(5))) + " Connector") self.__opcua_nodes = {} self._subscribed = {} self.data_to_send = [] self.__sub_handler = SubHandler(self) self.__stopped = False self.__connected = False self.daemon = True def is_connected(self): return self.__connected def open(self): self.__stopped = False self.start() log.info("Starting OPC-UA Connector") def run(self): while not self.__connected: try: self.__connected = self.client.connect() try: self.client.load_type_definitions() except Exception as e: log.debug(e) log.debug("Error on loading type definitions.") log.debug(self.client.get_namespace_array()[-1]) log.debug( self.client.get_namespace_index( self.client.get_namespace_array()[-1])) except ConnectionRefusedError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except OSError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except Exception as e: log.debug("error on connection to OPC-UA server.") log.error(e) time.sleep(10) else: self.__connected = True log.info("OPC-UA connector %s connected to server %s", self.get_name(), self.__server_conf.get("url")) self.__opcua_nodes["root"] = self.client.get_objects_node() self.__opcua_nodes["objects"] = self.client.get_objects_node() if not self.__server_conf.get("disableSubscriptions", False): self.__sub = self.client.create_subscription( self.__server_conf.get("subCheckPeriodInMillis", 500), self.__sub_handler) else: self.__sub = False self.__scan_nodes_from_config() self.__previous_scan_time = time.time() * 1000 log.debug('Subscriptions: %s', self.subscribed) log.debug("Available methods: %s", self.__available_object_resources) while not self.__stopped: try: time.sleep(.1) self.__check_connection() if not self.__connected and not self.__stopped: self.client.connect() elif not self.__stopped: if self.__server_conf.get( "disableSubscriptions", False ) and time.time( ) * 1000 - self.__previous_scan_time > self.__server_conf.get( "scanPeriodInMillis", 60000): self.__scan_nodes_from_config() self.__previous_scan_time = time.time() * 1000 if self.data_to_send: self.__gateway.send_to_storage(self.get_name(), self.data_to_send.pop()) if self.__stopped: self.close() break except (KeyboardInterrupt, SystemExit): self.close() raise except ConnectionRefusedError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except Exception as e: self.close() log.exception(e) def __check_connection(self): try: node = self.client.get_root_node() node.get_children() self.__connected = True except ConnectionRefusedError: self.__connected = False self._subscribed = {} self.__sub = None except OSError: self.__connected = False self._subscribed = {} self.__sub = None except TimeoutError: self.__connected = False self._subscribed = {} self.__sub = None except AttributeError: self.__connected = False self._subscribed = {} self.__sub = None except Exception as e: self.__connected = False self._subscribed = {} self.__sub = None log.exception(e) def close(self): self.__stopped = True if self.__connected: self.client.disconnect() self.__connected = False log.info('%s has been stopped.', self.get_name()) def get_name(self): return self.name def on_attributes_update(self, content): log.debug(content) try: for server_variables in self.__available_object_resources[ content["device"]]['variables']: for attribute in content["data"]: for variable in server_variables: if attribute == variable: server_variables[variable].set_value( content["data"][variable]) except Exception as e: log.exception(e) def server_side_rpc_handler(self, content): try: for method in self.__available_object_resources[ content["device"]]['methods']: rpc_method = content["data"].get("method") if rpc_method is not None and method.get( rpc_method) is not None: arguments_from_config = method["arguments"] arguments = content["data"].get( "params") if content["data"].get( "params") is not None else arguments_from_config try: if type(arguments) is list: result = method["node"].call_method( method[rpc_method], *arguments) elif arguments is not None: result = method["node"].call_method( method[rpc_method], arguments) else: result = method["node"].call_method( method[rpc_method]) self.__gateway.send_rpc_reply( content["device"], content["data"]["id"], { content["data"]["method"]: result, "code": 200 }) log.debug("method %s result is: %s", method[rpc_method], result) except Exception as e: log.exception(e) self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], { "error": str(e), "code": 500 }) else: log.error("Method %s not found for device %s", rpc_method, content["device"]) self.__gateway.send_rpc_reply( content["device"], content["data"]["id"], { "error": "%s - Method not found" % (rpc_method), "code": 404 }) except Exception as e: log.exception(e) def __scan_nodes_from_config(self): try: if self.__interest_nodes: for device_object in self.__interest_nodes: for current_device in device_object: try: device_configuration = device_object[ current_device] devices_info_array = self.__search_general_info( device_configuration) for device_info in devices_info_array: if device_info is not None and device_info.get( "deviceNode") is not None: self.__search_nodes_and_subscribe( device_info) self.__save_methods(device_info) self.__search_attribute_update_variables( device_info) else: log.error( "Device node is None, please check your configuration." ) log.debug( "Current device node is: %s", str( device_configuration.get( "deviceNodePattern"))) break except Exception as e: log.exception(e) log.debug(self.__interest_nodes) except Exception as e: log.exception(e) def __search_nodes_and_subscribe(self, device_info): information_types = { "attributes": "attributes", "timeseries": "telemetry" } for information_type in information_types: for information in device_info["configuration"][information_type]: information_key = information["key"] config_path = TBUtility.get_value(information["path"], get_tag=True) information_path = self._check_path(config_path, device_info["deviceNode"]) information["path"] = '${%s}' % information_path information_nodes = [] self.__search_node(device_info["deviceNode"], information_path, result=information_nodes) for information_node in information_nodes: if information_node is not None: information_value = information_node.get_value() log.debug( "Node for %s \"%s\" with path: %s - FOUND! Current values is: %s", information_type, information_key, information_path, str(information_value)) if device_info.get("uplink_converter") is None: configuration = { **device_info["configuration"], "deviceName": device_info["deviceName"], "deviceType": device_info["deviceType"] } if device_info["configuration"].get( 'converter') is None: converter = OpcUaUplinkConverter(configuration) else: converter = TBUtility.check_and_import( self._connector_type, configuration) device_info["uplink_converter"] = converter else: converter = device_info["uplink_converter"] self.subscribed[information_node] = { "converter": converter, "path": information_path, "config_path": config_path } if not device_info.get( information_types[information_type]): device_info[ information_types[information_type]] = [] converted_data = converter.convert( (config_path, information_path), information_value) self.statistics['MessagesReceived'] += 1 self.data_to_send.append(converted_data) self.statistics['MessagesSent'] += 1 if self.__sub is None: self.__sub = self.client.create_subscription( self.__server_conf.get( "subCheckPeriodInMillis", 500), self.__sub_handler) if self.__sub: self.__sub.subscribe_data_change(information_node) log.debug("Added subscription to node: %s", str(information_node)) log.debug("Data to ThingsBoard: %s", converted_data) else: log.error( "Node for %s \"%s\" with path %s - NOT FOUND!", information_type, information_key, information_path) def __save_methods(self, device_info): try: if self.__available_object_resources.get( device_info["deviceName"]) is None: self.__available_object_resources[ device_info["deviceName"]] = {} if self.__available_object_resources[ device_info["deviceName"]].get("methods") is None: self.__available_object_resources[ device_info["deviceName"]]["methods"] = [] if device_info["configuration"].get("rpc_methods"): node = device_info["deviceNode"] for method_object in device_info["configuration"][ "rpc_methods"]: method_node_path = self._check_path( method_object["method"], node) methods = [] self.__search_node(node, method_node_path, True, result=methods) for method in methods: if method is not None: node_method_name = method.get_display_name().Text self.__available_object_resources[ device_info["deviceName"]]["methods"].append({ node_method_name: method, "node": node, "arguments": method_object.get("arguments") }) else: log.error( "Node for method with path %s - NOT FOUND!", method_node_path) except Exception as e: log.exception(e) def __search_attribute_update_variables(self, device_info): try: if device_info["configuration"].get("attributes_updates"): node = device_info["deviceNode"] device_name = device_info["deviceName"] if self.__available_object_resources.get(device_name) is None: self.__available_object_resources[device_name] = {} if self.__available_object_resources[device_name].get( "variables") is None: self.__available_object_resources[device_name][ "variables"] = [] for attribute_update in device_info["configuration"][ "attributes_updates"]: attribute_path = self._check_path( attribute_update["attributeOnDevice"], node) attribute_nodes = [] self.__search_node(node, attribute_path, result=attribute_nodes) for attribute_node in attribute_nodes: if attribute_node is not None: self.__available_object_resources[device_name][ "variables"].append({ attribute_update["attributeOnThingsBoard"]: attribute_node }) else: log.error( "Attribute update node with path \"%s\" - NOT FOUND!", attribute_path) except Exception as e: log.exception(e) def __search_general_info(self, device): result = [] match_devices = [] self.__search_node(self.__opcua_nodes["root"], TBUtility.get_value(device["deviceNodePattern"], get_tag=True), result=match_devices) for device_node in match_devices: if device_node is not None: result_device_dict = { "deviceName": None, "deviceType": None, "deviceNode": device_node, "configuration": deepcopy(device) } name_pattern_config = device["deviceNamePattern"] name_expression = TBUtility.get_value(name_pattern_config, get_tag=True) if "${" in name_pattern_config and "}" in name_pattern_config: log.debug("Looking for device name") name_path = self._check_path(name_expression, device_node) device_name_node = [] self.__search_node(device_node, name_path, result=device_name_node) device_name_node = device_name_node[0] if device_name_node is not None: device_name_from_node = device_name_node.get_value() full_device_name = name_pattern_config.replace( "${" + name_expression + "}", str(device_name_from_node)).replace( name_expression, str(device_name_from_node)) else: log.error( "Device name node not found with expression: %s", name_expression) return else: full_device_name = name_expression result_device_dict["deviceName"] = full_device_name log.debug("Device name: %s", full_device_name) if device.get("deviceTypePattern"): device_type_expression = TBUtility.get_value( device["deviceTypePattern"], get_tag=True) if "${" in device_type_expression and "}" in device_type_expression: type_path = self._check_path(device_type_expression, device_node) device_type_node = [] self.__search_node(device_node, type_path, result=device_type_node) device_type_node = device_type_node[0] if device_type_node is not None: device_type = device_type_node.get_value() full_device_type = device_type_expression.replace( "${" + device_type_expression + "}", device_type).replace(device_type_expression, device_type) else: log.error( "Device type node not found with expression: %s", device_type_expression) full_device_type = "default" else: full_device_type = device_type_expression result_device_dict["deviceType"] = full_device_type log.debug("Device type: %s", full_device_type) else: result_device_dict["deviceType"] = "default" result.append(result_device_dict) else: log.error( "Device node not found with expression: %s", TBUtility.get_value(device["deviceNodePattern"], get_tag=True)) return result def __search_node(self, current_node, fullpath, search_method=False, result=[]): fullpath_pattern = regex.compile(fullpath) try: for child_node in current_node.get_children(): new_node = self.client.get_node(child_node) new_node_path = '\\\\.'.join( char.split(":")[1] for char in new_node.get_path(200000, True)) if self.__show_map: log.debug("SHOW MAP: Current node path: %s", new_node_path) new_node_class = new_node.get_node_class() # regex_fullmatch = re.fullmatch(fullpath, new_node_path.replace('\\\\.', '.')) or new_node_path.replace('\\\\', '\\') == fullpath regex_fullmatch = regex.fullmatch(fullpath_pattern, new_node_path.replace('\\\\.', '.')) or \ new_node_path.replace('\\\\', '\\') == fullpath.replace('\\\\', '\\') or \ new_node_path.replace('\\\\', '\\') == fullpath regex_search = fullpath_pattern.fullmatch(new_node_path.replace('\\\\.', '.'), partial=True) or \ new_node_path.replace('\\\\', '\\') in fullpath.replace('\\\\', '\\') # regex_search = re.search(new_node_path, fullpath.replace('\\\\', '\\')) if regex_fullmatch: if self.__show_map: log.debug( "SHOW MAP: Current node path: %s - NODE FOUND", new_node_path.replace('\\\\', '\\')) result.append(new_node) elif regex_search: if self.__show_map: log.debug( "SHOW MAP: Current node path: %s - NODE FOUND", new_node_path) if new_node_class == ua.NodeClass.Object: if self.__show_map: log.debug("SHOW MAP: Search in %s", new_node_path) self.__search_node(new_node, fullpath, result=result) elif new_node_class == ua.NodeClass.Variable: log.debug("Found in %s", new_node_path) result.append(new_node) elif new_node_class == ua.NodeClass.Method and search_method: log.debug("Found in %s", new_node_path) result.append(new_node) except Exception as e: log.exception(e) def _check_path(self, config_path, node): if re.search("^root", config_path.lower()) is None: node_path = '\\\\.'.join( char.split(":")[1] for char in node.get_path(200000, True)) if config_path[-3:] != '\\.': information_path = node_path + '\\\\.' + config_path.replace( '\\', '\\\\') else: information_path = node_path + config_path.replace( '\\', '\\\\') else: information_path = config_path return information_path[:] @property def subscribed(self): return self._subscribed
class NodeXMLExporter: def __init__(self): self.nodes = [] self.namespaces = {} self.visited = [] self.client = None self.logger = logging.getLogger(__name__) self.logger.setLevel(logging.INFO) def iterater_over_child_nodes(self, node): self.nodes.append(node) self.logger.debug("Add %s" % node) # iterate over all referenced nodes (31), only hierarchical references (33) for child in node.get_children(refs=33): if child not in self.nodes: self.iterater_over_child_nodes(child) def export_xml(self, namespaces=None, output_file="export.xml"): if namespaces: self.logger.info("Export only NS %s" % namespaces) nodes = [node for node in self.nodes if node.nodeid.NamespaceIndex in namespaces] else: nodes = self.nodes self.logger.info("Export nodes to %s" % output_file) exp = XmlExporter(self.client) exp.build_etree(nodes) exp.write_xml(output_file) self.logger.info("Export finished") def import_nodes(self, server_url="opc.tcp://localhost:16664"): from opcua.crypto import security_policies import types from opcua.ua.uaprotocol_hand import CryptographyNone self.client = Client(server_url) # Fix symmetric_key_size (not 0) of securityPolicy sec_policy = security_policies.SecurityPolicy() sec_policy.symmetric_key_size = 8 self.client.security_policy = sec_policy # Fix signature method of CryptographyNone def signature(self, data): return None fixed_signature = types.MethodType(signature, CryptographyNone) self.client.security_policy.asymmetric_cryptography.signature = fixed_signature try: self.client.connect() except Exception as e: self.logger.error("No connection established", e) self.logger.error(e) self.logger.error("Exiting ...") sys.exit() self.logger.info("Client connected to %s" % server_url) for ns in self.client.get_namespace_array(): self.namespaces[self.client.get_namespace_index(ns)] = ns root = self.client.get_root_node() self.logger.info("Starting to collect nodes. This may take some time ...") self.iterater_over_child_nodes(root) self.logger.info("All nodes collected") def statistics(self): types = {} for node in self.nodes: try: node_class = str(node.get_node_class()) ns = node.nodeid.NamespaceIndex if ns not in types: types[ns] = {} if node_class not in types[ns]: types[ns][node_class] = 1 else: types[ns][node_class] += 1 except Exception as e: self.logger.info("some error with %s: %s" % (node,e)) for ns in types: self.logger.info("NS%d (%s)" % (ns, self.namespaces[ns])) for type in types[ns]: self.logger.info("\t%s:\t%d" % (type, types[ns][type])) self.logger.info("\tTOTAL in namespace: %d" % len(self.nodes))
from opcua import Client client = Client('opc.tcp://127.0.0.1:51213') client.connect() client.get_namespace_array() objects_node = client.get_objects_node() # Print children nodes print(objects_node.get_children(), flush=True) # Get directly a specific node temperature = client.get_node('ns=2;s="temperature"') # get a node value (if it's a variable) temperature.get_value() light_bulb_state = client.get_node('ns=2;s="light_bulb_state"') # If the variable is writable, its value can be set from the client side light_bulb_state.set_value(True) # Close client session (the servere remains connected) client.close_session()
class OpcClient(): """A simplified OpClient. This is a opc client that can be leveraged to subscribe to the OPC data change notifications. You should interact with the client using * add_notification_handler * subscribe_variable * unsubscribe_variable So typically: ``` # setup client cl = OpcClient(...) # define handler def handler(update): # data update : {'node': <node>, 'timestamp': <datetime>, 'value': <value>, 'data': <data>} # do something with update here # add handler cl.add_notification_handler(handler) # subscribe to variable cl.subscribe_variable(cl.get_node("ns=3;i=2002")) time.sleep(100) cl.disconnect() ``` *Note* Make sure to call disconnect before shutting down in order to ensure a clean close. See: https://github.com/FreeOpcUa/python-opcua Attributes ---------- _address : str The connection string address _address_obfuscated : str The connection string address obfuscating a potnetial password _client : OPCLibClient The opc lib client _subscription : ua.Subscription The ua subscription handle that can be used to create new subscritions or unsubscribe _sub_handles : dict A dictionary mapping ua.Node variables to subscription handles to be able to unsubscribe from them again _handlers : list A list of handler functions """ def __init__(self, host, port, path='/', username=None, password=None): """Create a new OpClient. This will create a new opc client that can be leveraged to subscribe to the OPC data change notifications. Parameters ---------- host : str The host name of the opc server. port : int The port of the opc server. path : str The path to add to the server as base username : str|None A username to use for authorization. password : str|None A password to use for authorization. """ self._client = None self._subscription = None self._sub_handles = {} self._handlers = [] # setup authorization auth = '' if username is not None: auth = '{}:{}@'.format(username, password) # define address self._address = self._address_obfuscated = "opc.tcp://{}{}:{}{}".format( auth, host, port, path) if username is not None and password is not None: self._address_obfuscated = self._address.replace(password, '***') Logger.trace('Created OpcClient', fields={'address': self._address_obfuscated}) # setup client self.init_client() # setup subscriptions self.init_subscriptions() def init_client(self): """Initialize the client. This will connect to the client using the address. This is required to be called before anything else interacting with the opc server is done. """ try: self._client = OPCLibClient(self._address) self._client.connect() self._client.load_type_definitions() self.namespace = self._client.get_namespace_array() Logger.trace('Connection established') except Exception as e: Logger.error('Failed connecting to address "{}"'.format( self._address_obfuscated)) Logger.error('{}'.format(e)) raise e def log_info(self): root = self._client.get_root_node() objects = self._client.get_objects_node() children = root.get_children() # print '{}:{}'.format(namespace[children[1].nodeid.NamespaceIndex], children[1].nodeid.Identifier) # print self._client.get_node("ns=0;i=86") Logger.debug("Retrieved Client Info", fields={ 'root': root, 'objects': objects, 'children': children, 'namespace': self.namespace }) def full_uri(self, node): """Resolve the full uri of a ua.Node. Parameters ---------- node : ua.Node A node from the OPC interface. Returns ------- str : The namespace prefixed full uri of the entity. """ return '{}:{}'.format(self.namespace[node.nodeid.NamespaceIndex], node.nodeid.Identifier) def get_by_uri(self, uri): """Resolve the full uri to a ua.Node. Parameters ---------- uri : str The uri to the ua.Node. Returns ------- ua.Node : The resolved node. """ ns_match = None for ns in self.namespace: if '{}:'.format(ns) in uri: ns_match = ns break if ns_match is None: raise RuntimeError('Namespace of "{}" not available'.format(uri)) ns_idx = self._client.get_namespace_index(ns_match) identifier = uri.replace('{}:'.format(ns_match), '') try: int(identifier) is_int = True except: is_int = False substituted = 'ns={};{}={}'.format(ns_idx, 'i' if is_int else 's', identifier) return self._client.get_node(substituted) def init_subscriptions(self): """Initialize the subscriptions. This will initialize a subscription handler and afterwards will be ready to subscribe to specific variables. """ if self._client is None: raise RuntimeError('Client not initialized yet.') self._subscription = self._client.create_subscription(500, self) Logger.trace('Base Subscription established') def subscribe_variable(self, variable): """Subscribe to a OPC variable. This will subscribe the client to the given variable and any data change notification will be published. Parameters ---------- variable : ua.Node The ua node, e.g. client.get_node(ua.NodeId(1002, 2)) or client.get_node("ns=3;i=2002") """ if self._subscription is None: raise RuntimeError('Subscriptions not initialized yet.') if variable in self._sub_handles.keys(): Logger.info('Already subscribed to "{}"'.format(variable)) return self._sub_handles[variable] = self._subscription.subscribe_data_change( variable) def unsubscribe_variable(self, variable): """Unsubscribe from a OPC variable. This will unsubscribe the client from the given variable. Parameters ---------- variable : ua.Node The ua node, e.g. client.get_node(ua.NodeId(1002, 2)) or client.get_node("ns=3;i=2002") """ if self._subscription is None: raise RuntimeError('Subscriptions not initialized yet.') if variable not in self._sub_handles.keys(): Logger.info('Not subscribed to "{}"'.format(variable)) return self._subscription.unsubscribe(self._sub_handles[variable]) del self._sub_handles[variable] def disconnect(self): """Disconnect the client. """ if self._client is None: return try: self._client.disconnect() except Exception: pass def datachange_notification(self, node, value, data): """Receiver from the OPC client. This gets called by OPC client on subscription notification. Parameter --------- node : ua.Node The node from which we received the data. value : any The value notification data : any The data of the notification """ timestamp = datetime.now() if hasattr(data, 'MonitoredItemNotification') and \ hasattr(data.MonitoredItemNotification, 'SourceTimestamp'): timestamp = data.MonitoredItemNotification.SourceTimestamp Logger.debug('OpcClient: Received data change notification', fields={ 'node': node, 'value': value, 'data': data, 'timestamp': timestamp.isoformat() }) # send update to handlers update = { 'node': node, 'value': value, 'data': data, 'timestamp': timestamp } for hdl in self._handlers: hdl(update) def add_notification_handler(self, handler): """Add a handler function. This handler `def handler(update)` will be called upon reception of a notification from the OPC Client. The update will have the following data: ```{ 'node': <node>, 'value': <value>, 'data': <data>, 'timestamp': <timestamp> }``` """ self._handlers.append(handler)
class OpcUaConnector(Thread, Connector): def __init__(self, gateway, config, connector_type): self._connector_type = connector_type self.statistics = {'MessagesReceived': 0, 'MessagesSent': 0} super().__init__() self.__gateway = gateway self.__server_conf = config.get("server") self.__interest_nodes = [] self.__available_object_resources = {} self.__show_map = self.__server_conf.get("showMap", False) self.__previous_scan_time = 0 for mapping in self.__server_conf["mapping"]: if mapping.get("deviceNodePattern") is not None: self.__interest_nodes.append( {mapping["deviceNodePattern"]: mapping}) else: log.error( "deviceNodePattern in mapping: %s - not found, add property deviceNodePattern to processing this mapping", dumps(mapping)) if "opc.tcp" not in self.__server_conf.get("url"): self.__opcua_url = "opc.tcp://" + self.__server_conf.get("url") else: self.__opcua_url = self.__server_conf.get("url") self.client = Client( self.__opcua_url, timeout=self.__server_conf.get("timeoutInMillis", 4000) / 1000) if self.__server_conf["identity"].get("type") == "cert.PEM": try: ca_cert = self.__server_conf["identity"].get("caCert") private_key = self.__server_conf["identity"].get("privateKey") cert = self.__server_conf["identity"].get("cert") security_mode = self.__server_conf["identity"].get( "mode", "SignAndEncrypt") policy = self.__server_conf["security"] if cert is None or private_key is None: log.exception( "Error in ssl configuration - cert or privateKey parameter not found" ) raise RuntimeError( "Error in ssl configuration - cert or privateKey parameter not found" ) security_string = policy + ',' + security_mode + ',' + cert + ',' + private_key if ca_cert is not None: security_string = security_string + ',' + ca_cert self.client.set_security_string(security_string) except Exception as e: log.exception(e) if self.__server_conf["identity"].get("username"): self.client.set_user( self.__server_conf["identity"].get("username")) if self.__server_conf["identity"].get("password"): self.client.set_password( self.__server_conf["identity"].get("password")) self.setName( self.__server_conf.get( "name", 'OPC-UA ' + ''.join(choice(ascii_lowercase) for _ in range(5))) + " Connector") self.__opcua_nodes = {} self._subscribed = {} self.__sub = None self.__sub_handler = SubHandler(self) self.data_to_send = [] self.__stopped = False self.__connected = False self.daemon = True def is_connected(self): return self.__connected def open(self): self.__stopped = False self.start() log.info("Starting OPC-UA Connector") def run(self): while not self.__connected: try: self.client.connect() try: self.client.load_type_definitions() except Exception as e: log.debug(e) log.debug("Error on loading type definitions.") log.debug(self.client.get_namespace_array()[-1]) log.debug( self.client.get_namespace_index( self.client.get_namespace_array()[-1])) except ConnectionRefusedError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except OSError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except Exception as e: log.debug("error on connection to OPC-UA server.") log.error(e) time.sleep(10) else: self.__connected = True log.info("OPC-UA connector %s connected to server %s", self.get_name(), self.__server_conf.get("url")) self.__initialize_client() while not self.__stopped: try: time.sleep(.1) self.__check_connection() if not self.__connected and not self.__stopped: self.client.connect() self.__initialize_client() log.info("Reconnected to the OPC-UA server - %s", self.__server_conf.get("url")) elif not self.__stopped: if self.__server_conf.get( "disableSubscriptions", False ) and time.time( ) * 1000 - self.__previous_scan_time > self.__server_conf.get( "scanPeriodInMillis", 60000): self.scan_nodes_from_config() self.__previous_scan_time = time.time() * 1000 # giusguerrini, 2020-09-24: Fix: flush event set and send all data to platform, # so data_to_send doesn't grow indefinitely in case of more than one value change # per cycle, and platform doesn't lose events. # NOTE: possible performance improvement: use a map to store only one event per # variable to reduce frequency of messages to platform. while self.data_to_send: self.__gateway.send_to_storage(self.get_name(), self.data_to_send.pop()) if self.__stopped: self.close() break except (KeyboardInterrupt, SystemExit): self.close() raise except FuturesTimeoutError: self.__check_connection() except Exception as e: log.error( "Connection failed on connection to OPC-UA server with url %s", self.__server_conf.get("url")) log.exception(e) self.client = Client( self.__opcua_url, timeout=self.__server_conf.get("timeoutInMillis", 4000) / 1000) self._subscribed = {} self.__available_object_resources = {} time.sleep(10) def __check_connection(self): try: node = self.client.get_root_node() node.get_children() if not self.__server_conf.get("disableSubscriptions", False) and ( not self.__connected or not self.subscribed): self.__sub = self.client.create_subscription( self.__server_conf.get("subCheckPeriodInMillis", 500), self.__sub_handler) self.__connected = True except ConnectionRefusedError: self.__connected = False self._subscribed = {} self.__available_object_resources = {} self.__sub = None except OSError: self.__connected = False self._subscribed = {} self.__available_object_resources = {} self.__sub = None except FuturesTimeoutError: self.__connected = False self._subscribed = {} self.__available_object_resources = {} self.__sub = None except AttributeError: self.__connected = False self._subscribed = {} self.__available_object_resources = {} self.__sub = None except Exception as e: self.__connected = False self._subscribed = {} self.__available_object_resources = {} self.__sub = None log.exception(e) def close(self): self.__stopped = True if self.__connected: self.client.disconnect() self.__connected = False log.info('%s has been stopped.', self.get_name()) def get_name(self): return self.name def on_attributes_update(self, content): log.debug(content) try: for server_variables in self.__available_object_resources[ content["device"]]['variables']: for attribute in content["data"]: for variable in server_variables: if attribute == variable: try: server_variables[variable].set_value( content["data"][variable]) except Exception: server_variables[variable].set_attribute( ua.AttributeIds.Value, ua.DataValue(content["data"][variable])) except Exception as e: log.exception(e) def server_side_rpc_handler(self, content): try: rpc_method = content["data"].get("method") for method in self.__available_object_resources[ content["device"]]['methods']: if rpc_method is not None and method.get( rpc_method) is not None: arguments_from_config = method["arguments"] arguments = content["data"].get( "params") if content["data"].get( "params") is not None else arguments_from_config try: if isinstance(arguments, list): result = method["node"].call_method( method[rpc_method], *arguments) elif arguments is not None: try: result = method["node"].call_method( method[rpc_method], arguments) except ua.UaStatusCodeError as e: if "BadTypeMismatch" in str(e) and isinstance( arguments, int): result = method["node"].call_method( method[rpc_method], float(arguments)) else: result = method["node"].call_method( method[rpc_method]) self.__gateway.send_rpc_reply( content["device"], content["data"]["id"], { content["data"]["method"]: result, "code": 200 }) log.debug("method %s result is: %s", method[rpc_method], result) except Exception as e: log.exception(e) self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], { "error": str(e), "code": 500 }) else: log.error("Method %s not found for device %s", rpc_method, content["device"]) self.__gateway.send_rpc_reply( content["device"], content["data"]["id"], { "error": "%s - Method not found" % (rpc_method), "code": 404 }) except Exception as e: log.exception(e) def __initialize_client(self): self.__opcua_nodes["root"] = self.client.get_objects_node() self.__opcua_nodes["objects"] = self.client.get_objects_node() self.scan_nodes_from_config() self.__previous_scan_time = time.time() * 1000 log.debug('Subscriptions: %s', self.subscribed) log.debug("Available methods: %s", self.__available_object_resources) def scan_nodes_from_config(self): try: if self.__interest_nodes: for device_object in self.__interest_nodes: for current_device in device_object: try: device_configuration = device_object[ current_device] devices_info_array = self.__search_general_info( device_configuration) for device_info in devices_info_array: if device_info is not None and device_info.get( "deviceNode") is not None: self.__search_nodes_and_subscribe( device_info) self.__save_methods(device_info) self.__search_attribute_update_variables( device_info) else: log.error( "Device node is None, please check your configuration." ) log.debug( "Current device node is: %s", str( device_configuration.get( "deviceNodePattern"))) break except BrokenPipeError: log.debug("Broken Pipe. Connection lost.") except OSError: log.debug("Stop on scanning.") except FuturesTimeoutError: self.__check_connection() except Exception as e: log.exception(e) log.debug(self.__interest_nodes) except Exception as e: log.exception(e) def __search_nodes_and_subscribe(self, device_info): sub_nodes = [] information_types = { "attributes": "attributes", "timeseries": "telemetry" } for information_type in information_types: for information in device_info["configuration"][information_type]: information_key = information["key"] config_path = TBUtility.get_value(information["path"], get_tag=True) information_path = self._check_path(config_path, device_info["deviceNode"]) information["path"] = '${%s}' % information_path information_nodes = [] self.__search_node(device_info["deviceNode"], information_path, result=information_nodes) for information_node in information_nodes: if information_node is not None: try: information_value = information_node.get_value() except: log.error("Err get_value: %s", str(information_node)) continue log.debug( "Node for %s \"%s\" with path: %s - FOUND! Current values is: %s", information_type, information_key, information_path, str(information_value)) if device_info.get("uplink_converter") is None: configuration = { **device_info["configuration"], "deviceName": device_info["deviceName"], "deviceType": device_info["deviceType"] } if device_info["configuration"].get( 'converter') is None: converter = OpcUaUplinkConverter(configuration) else: converter = TBModuleLoader.import_module( self._connector_type, configuration) device_info["uplink_converter"] = converter else: converter = device_info["uplink_converter"] self.subscribed[information_node] = { "converter": converter, "path": information_path, "config_path": config_path } if not device_info.get( information_types[information_type]): device_info[ information_types[information_type]] = [] converted_data = converter.convert( (config_path, information_path), information_value) self.statistics['MessagesReceived'] = self.statistics[ 'MessagesReceived'] + 1 self.data_to_send.append(converted_data) self.statistics['MessagesSent'] = self.statistics[ 'MessagesSent'] + 1 log.debug("Data to ThingsBoard: %s", converted_data) if not self.__server_conf.get("disableSubscriptions", False): sub_nodes.append(information_node) else: log.error( "Node for %s \"%s\" with path %s - NOT FOUND!", information_type, information_key, information_path) if not self.__server_conf.get("disableSubscriptions", False): if self.__sub is None: self.__sub = self.client.create_subscription( self.__server_conf.get("subCheckPeriodInMillis", 500), self.__sub_handler) if sub_nodes: self.__sub.subscribe_data_change(sub_nodes) log.debug("Added subscription to nodes: %s", str(sub_nodes)) def __save_methods(self, device_info): try: if self.__available_object_resources.get( device_info["deviceName"]) is None: self.__available_object_resources[ device_info["deviceName"]] = {} if self.__available_object_resources[ device_info["deviceName"]].get("methods") is None: self.__available_object_resources[ device_info["deviceName"]]["methods"] = [] if device_info["configuration"].get("rpc_methods", []): node = device_info["deviceNode"] for method_object in device_info["configuration"][ "rpc_methods"]: method_node_path = self._check_path( method_object["method"], node) methods = [] self.__search_node(node, method_node_path, True, result=methods) for method in methods: if method is not None: node_method_name = method.get_display_name().Text self.__available_object_resources[ device_info["deviceName"]]["methods"].append({ node_method_name: method, "node": node, "arguments": method_object.get("arguments") }) else: log.error( "Node for method with path %s - NOT FOUND!", method_node_path) except Exception as e: log.exception(e) def __search_attribute_update_variables(self, device_info): try: if device_info["configuration"].get("attributes_updates", []): node = device_info["deviceNode"] device_name = device_info["deviceName"] if self.__available_object_resources.get(device_name) is None: self.__available_object_resources[device_name] = {} if self.__available_object_resources[device_name].get( "variables") is None: self.__available_object_resources[device_name][ "variables"] = [] for attribute_update in device_info["configuration"][ "attributes_updates"]: attribute_path = self._check_path( attribute_update["attributeOnDevice"], node) attribute_nodes = [] self.__search_node(node, attribute_path, result=attribute_nodes) for attribute_node in attribute_nodes: if attribute_node is not None: self.__available_object_resources[device_name][ "variables"].append({ attribute_update["attributeOnThingsBoard"]: attribute_node }) else: log.error( "Attribute update node with path \"%s\" - NOT FOUND!", attribute_path) except Exception as e: log.exception(e) def __search_general_info(self, device): result = [] match_devices = [] self.__search_node(self.__opcua_nodes["root"], TBUtility.get_value(device["deviceNodePattern"], get_tag=True), result=match_devices) for device_node in match_devices: if device_node is not None: result_device_dict = { "deviceName": None, "deviceType": None, "deviceNode": device_node, "configuration": deepcopy(device) } name_pattern_config = device["deviceNamePattern"] name_expression = TBUtility.get_value(name_pattern_config, get_tag=True) if "${" in name_pattern_config and "}" in name_pattern_config: log.debug("Looking for device name") device_name_from_node = "" if name_expression == "$DisplayName": device_name_from_node = device_node.get_display_name( ).Text elif name_expression == "$BrowseName": device_name_from_node = device_node.get_browse_name( ).Name elif name_expression == "$NodeId.Identifier": device_name_from_node = str( device_node.nodeid.Identifier) else: name_path = self._check_path(name_expression, device_node) device_name_node = [] self.__search_node(device_node, name_path, result=device_name_node) device_name_node = device_name_node[0] if device_name_node is not None: device_name_from_node = device_name_node.get_value( ) if device_name_from_node == "": log.error( "Device name node not found with expression: %s", name_expression) return None full_device_name = name_pattern_config.replace( "${" + name_expression + "}", str(device_name_from_node)).replace( name_expression, str(device_name_from_node)) else: full_device_name = name_expression result_device_dict["deviceName"] = full_device_name log.debug("Device name: %s", full_device_name) if device.get("deviceTypePattern"): device_type_expression = TBUtility.get_value( device["deviceTypePattern"], get_tag=True) if "${" in device_type_expression and "}" in device_type_expression: type_path = self._check_path(device_type_expression, device_node) device_type_node = [] self.__search_node(device_node, type_path, result=device_type_node) device_type_node = device_type_node[0] if device_type_node is not None: device_type = device_type_node.get_value() full_device_type = device_type_expression.replace( "${" + device_type_expression + "}", device_type).replace(device_type_expression, device_type) else: log.error( "Device type node not found with expression: %s", device_type_expression) full_device_type = "default" else: full_device_type = device_type_expression result_device_dict["deviceType"] = full_device_type log.debug("Device type: %s", full_device_type) else: result_device_dict["deviceType"] = "default" result.append(result_device_dict) else: log.error( "Device node not found with expression: %s", TBUtility.get_value(device["deviceNodePattern"], get_tag=True)) return result # # get fullpath of node as string # # this is verry slow # path = '\\.'.join(char.split(":")[1] for char in node.get_path(200000, True)) # i think we don't need \\. # def get_node_path(self, node): ID = node.nodeid.Identifier if ID == 85: return 'Root.Objects' # this is Root if type(ID) == int: ID = node.get_browse_name( ).Name # for int Identifer we take browsename return 'Root.Objects.' + ID def __search_node(self, current_node, fullpath, search_method=False, result=None): if result is None: result = [] try: if regex.match(r"ns=\d*;[isgb]=.*", fullpath, regex.IGNORECASE): if self.__show_map: log.debug("Looking for node with config") node = self.client.get_node(fullpath) if node is None: log.warning("NODE NOT FOUND - using configuration %s", fullpath) else: log.debug("Found in %s", node) result.append(node) else: fullpath_pattern = regex.compile(fullpath) full1 = fullpath.replace('\\\\.', '.') #current_node_path = '\\.'.join(char.split(":")[1] for char in current_node.get_path(200000, True)) current_node_path = self.get_node_path(current_node) # we are allways the parent child_node_parent_class = current_node.get_node_class() new_parent = current_node for child_node in current_node.get_children(): new_node_class = child_node.get_node_class() # this will not change you can do it outside th loop # basis Description of node.get_parent() function, sometime child_node.get_parent() return None #new_parent = child_node.get_parent() #if (new_parent is None): # child_node_parent_class = current_node.get_node_class() #else: # child_node_parent_class = child_node.get_parent().get_node_class() #current_node_path = '\\.'.join(char.split(":")[1] for char in current_node.get_path(200000, True)) #new_node_path = '\\\\.'.join(char.split(":")[1] for char in child_node.get_path(200000, True)) new_node_path = self.get_node_path(child_node) if child_node_parent_class == ua.NodeClass.View and new_parent is not None: parent_path = self.get_node_path(new_parent) #parent_path = '\\.'.join(char.split(":")[1] for char in new_parent.get_path(200000, True)) fullpath = fullpath.replace(current_node_path, parent_path) nnp1 = new_node_path.replace('\\\\.', '.') nnp2 = new_node_path.replace('\\\\', '\\') if self.__show_map: log.debug("SHOW MAP: Current node path: %s", new_node_path) regex_fullmatch = regex.fullmatch(fullpath_pattern, nnp1) or \ nnp2 == full1 or \ nnp2 == fullpath or \ nnp1 == full1 if regex_fullmatch: if self.__show_map: log.debug( "SHOW MAP: Current node path: %s - NODE FOUND", nnp2) result.append(child_node) else: regex_search = fullpath_pattern.fullmatch(nnp1, partial=True) or \ nnp2 in full1 or \ nnp1 in full1 if regex_search: if self.__show_map: log.debug( "SHOW MAP: Current node path: %s - NODE FOUND", new_node_path) if new_node_class == ua.NodeClass.Object: if self.__show_map: log.debug("SHOW MAP: Search in %s", new_node_path) self.__search_node(child_node, fullpath, result=result) elif new_node_class == ua.NodeClass.Variable: log.debug("Found in %s", new_node_path) result.append(child_node) elif new_node_class == ua.NodeClass.Method and search_method: log.debug("Found in %s", new_node_path) result.append(child_node) except CancelledError: log.error( "Request during search has been canceled by the OPC-UA server." ) except BrokenPipeError: log.error("Broken Pipe. Connection lost.") except OSError: log.debug("Stop on scanning.") except Exception as e: log.exception(e) def _check_path(self, config_path, node): if regex.match(r"ns=\d*;[isgb]=.*", config_path, regex.IGNORECASE): return config_path if re.search(r"^root", config_path.lower()) is None: node_path = self.get_node_path(node) #node_path = '\\\\.'.join(char.split(":")[1] for char in node.get_path(200000, True)) if config_path[-3:] != '\\.': information_path = node_path + '\\\\.' + config_path.replace( '\\', '\\\\') else: information_path = node_path + config_path.replace( '\\', '\\\\') else: information_path = config_path result = information_path[:] return result @property def subscribed(self): return self._subscribed
class OpcUaConnector(Thread, Connector): def __init__(self, gateway, config, connector_type): self.__connector_type = connector_type self.statistics = {'MessagesReceived': 0, 'MessagesSent': 0} super().__init__() self.__gateway = gateway self.__server_conf = config.get("server") self.__interest_nodes = [] self.__available_object_resources = {} for mapping in self.__server_conf["mapping"]: if mapping.get("deviceNodePattern") is not None: self.__interest_nodes.append( {mapping["deviceNodePattern"]: mapping}) else: log.error( "deviceNodePattern in mapping: %s - not found, add property deviceNodePattern to processing this mapping", dumps(mapping)) if "opc.tcp" not in self.__server_conf.get("url"): opcua_url = "opc.tcp://" + self.__server_conf.get("url") else: opcua_url = self.__server_conf.get("url") self.client = Client( opcua_url, timeout=self.__server_conf.get("timeoutInMillis", 4000) / 1000) if self.__server_conf["identity"]["type"] == "cert.PEM": try: ca_cert = self.__server_conf["identity"].get("caCert") private_key = self.__server_conf["identity"].get("privateKey") cert = self.__server_conf["identity"].get("cert") security_mode = self.__server_conf["identity"].get( "mode", "SignAndEncrypt") policy = self.__server_conf["security"] if cert is None or private_key is None: log.exception( "Error in ssl configuration - cert or privateKey parameter not found" ) raise security_string = policy + ',' + security_mode + ',' + cert + ',' + private_key if ca_cert is not None: security_string = security_string + ',' + ca_cert self.client.set_security_string(security_string) except Exception as e: log.exception(e) if self.__server_conf["identity"].get("username"): self.client.set_user( self.__server_conf["identity"].get("username")) if self.__server_conf["identity"].get("password"): self.client.set_password( self.__server_conf["identity"].get("password")) self.setName( self.__server_conf.get( "name", 'OPC-UA Default ' + ''.join(choice(ascii_lowercase) for _ in range(5))) + " Connector") self.__opcua_nodes = {} self._subscribed = {} self.data_to_send = [] self.__sub_handler = SubHandler(self) self.__stopped = False self.__connected = False self.daemon = True def is_connected(self): return self.__connected def open(self): self.__stopped = False self.start() log.info("Starting OPC-UA Connector") def run(self): while not self.__connected: try: self.__connected = self.client.connect() self.client.load_type_definitions() log.debug(self.client.get_namespace_array()[-1]) log.debug( self.client.get_namespace_index( self.client.get_namespace_array()[-1])) except ConnectionRefusedError: log.error( "Connection refused on connection to OPC-UA server with url %s", self.__server_conf.get("url")) time.sleep(10) except Exception as e: log.debug("error on connection to OPC-UA server.") log.error(e) time.sleep(10) else: self.__connected = True log.info("OPC-UA connector %s connected to server %s", self.get_name(), self.__server_conf.get("url")) self.__opcua_nodes["root"] = self.client.get_root_node() self.__opcua_nodes["objects"] = self.client.get_objects_node() sub = self.client.create_subscription( self.__server_conf.get("scanPeriodInMillis", 500), self.__sub_handler) self.__search_name(self.__opcua_nodes["objects"], 2) self.__search_tags(self.__opcua_nodes["objects"], 2, sub) log.debug('Subscriptions: %s', self.subscribed) log.debug("Available methods: %s", self.__available_object_resources) while True: try: time.sleep(1) if self.data_to_send: self.__gateway.send_to_storage(self.get_name(), self.data_to_send.pop()) if self.__stopped: break except (KeyboardInterrupt, SystemExit): self.close() raise except Exception as e: self.close() log.exception(e) def close(self): self.__stopped = True self.client.disconnect() self.__connected = False log.info('%s has been stopped.', self.get_name()) def get_name(self): return self.name def on_attributes_update(self, content): log.debug(content) try: for server_variables in self.__available_object_resources[ content["device"]]['variables']: for attribute in content["data"]: for variable in server_variables: if attribute == variable: server_variables[variable].set_value( content["data"][variable]) except Exception as e: log.exception(e) def server_side_rpc_handler(self, content): try: for method in self.__available_object_resources[ content["device"]]['methods']: rpc_method = content["data"].get("method") if rpc_method is not None and method.get( rpc_method) is not None: arguments = content["data"].get("params") if type(arguments) is list: result = method["node"].call_method( method[rpc_method], *arguments) elif arguments is not None: result = method["node"].call_method( method[rpc_method], arguments) else: result = method["node"].call_method(method[rpc_method]) self.__gateway.send_rpc_reply( content["device"], content["data"]["id"], {content["data"]["method"]: result}) log.debug("method %s result is: %s", method[rpc_method], result) except Exception as e: log.exception(e) def __search_name(self, node, recursion_level): try: for childId in node.get_children(): ch = self.client.get_node(childId) current_var_path = '.'.join( x.split(":")[1] for x in ch.get_path(20000, True)) if self.__interest_nodes: if ch.get_node_class() == ua.NodeClass.Object: for interest_node in self.__interest_nodes: for int_node in interest_node: subrecursion_level = recursion_level if subrecursion_level != recursion_level + len( interest_node[int_node] ["deviceNamePattern"].split("\\.")): if ch.get_display_name().Text in TBUtility.get_value(interest_node[int_node]["deviceNamePattern"], get_tag=True).split('.') or \ re.search(TBUtility.get_value(interest_node[int_node]["deviceNodePattern"], get_tag=True), ch.get_display_name().Text): self.__search_name( ch, subrecursion_level + 1) else: return elif ch.get_node_class() == ua.NodeClass.Variable: try: for interest_node in self.__interest_nodes: for int_node in interest_node: if interest_node[int_node].get( "deviceName") is None: try: name_pattern = TBUtility.get_value( interest_node[int_node] ["deviceNamePattern"], get_tag=True) log.debug(current_var_path) device_name_node = re.search( name_pattern.split('\\.')[-1], current_var_path) if device_name_node is not None: device_name = ch.get_value() if "${" + name_pattern + "}" in interest_node[ int_node][ "deviceNamePattern"]: full_device_name = interest_node[ int_node][ "deviceNamePattern"].replace( "${" + name_pattern + "}", device_name) elif device_name in interest_node[ int_node][ "deviceNamePattern"]: full_device_name = interest_node[ int_node][ "deviceNamePattern"].replace( name_pattern, device_name) else: log.error( "Name pattern not found." ) break interest_node[int_node][ "deviceName"] = full_device_name if self.__available_object_resources.get( full_device_name ) is None: self.__available_object_resources[ full_device_name] = { 'methods': [], 'variables': [] } if not self.__gateway.get_devices( ).get(full_device_name): self.__gateway.add_device( full_device_name, {"connector": None}) self.__gateway.update_device( full_device_name, "connector", self) else: try: if re.search( int_node.split( '\\.') [recursion_level - 2], ch. get_display_name( ).Text): self.__search_name( ch, recursion_level + 1) except IndexError: if re.search( int_node.split( '\\.')[-1], ch. get_display_name( ).Text): self.__search_name( ch, recursion_level + 1) except Exception as e: log.exception(e) else: break except BadWaitingForInitialData: pass elif not self.__interest_nodes: log.error( "Nodes in mapping not found, check your settings.") except Exception as e: log.exception(e) def __search_tags(self, node, recursion_level, sub=None): try: for childId in node.get_children(): ch = self.client.get_node(childId) current_var_path = '.'.join( x.split(":")[1] for x in ch.get_path(20000, True)) if self.__interest_nodes: if ch.get_node_class() == ua.NodeClass.Object: for interest_node in self.__interest_nodes: for int_node in interest_node: try: name_to_check = int_node.split('\\.')[ recursion_level - 1] if '\\.' in int_node else int_node name_to_check = int_node.split( '.' )[recursion_level - 1] if '.' in int_node else name_to_check except IndexError: name_to_check = int_node.split( '\\.' )[-1] if '\\.' in int_node else int_node name_to_check = int_node.split( '.' )[-1] if '.' in int_node else name_to_check if re.search(name_to_check, ch.get_display_name().Text): try: methods = ch.get_methods() for method in methods: self.__available_object_resources[ interest_node[int_node] ["deviceName"]][ "methods"].append({ method.get_display_name( ).Text: method, "node": ch }) except Exception as e: log.exception(e) for tag in interest_node[int_node][ "timeseries"] + interest_node[ int_node]["attributes"]: subrecursion_level = recursion_level if subrecursion_level != recursion_level + len( tag["path"].split("\\.")): self.__search_tags( ch, subrecursion_level + 1, sub) else: return self.__search_tags(ch, recursion_level + 1, sub) elif ch.get_node_class() == ua.NodeClass.Variable: try: for interest_node in self.__interest_nodes: for int_node in interest_node: if interest_node[int_node].get( "attributes_updates"): try: for attribute_update in interest_node[ int_node][ "attributes_updates"]: if attribute_update[ "attributeOnDevice"] == ch.get_display_name( ).Text: self.__available_object_resources[ interest_node[int_node] ["deviceName"]][ 'variables'].append({ attribute_update["attributeOnThingsBoard"]: ch, }) except Exception as e: log.exception(e) name_to_check = int_node.split( '\\.' )[-1] if '\\.' in int_node else int_node name_to_check = int_node.split( '.' )[-1] if '.' in int_node else name_to_check if re.search( name_to_check.replace('$', ''), current_var_path): tags = [] if interest_node[int_node].get( "attributes"): tags.extend(interest_node[int_node] ["attributes"]) if interest_node[int_node].get( "timeseries"): tags.extend(interest_node[int_node] ["timeseries"]) for tag in tags: target = TBUtility.get_value( tag["path"], get_tag=True) try: tag_name_for_check = target.split( '\\.' )[recursion_level - 1] if '\\.' in target else target tag_name_for_check = target.split( '.' )[recursion_level - 1] if '.' in target else tag_name_for_check except IndexError: tag_name_for_check = target.split( '\\.' )[-1] if '\\.' in target else target tag_name_for_check = target.split( '.' )[-1] if '.' in target else tag_name_for_check current_node_name = ch.get_display_name( ).Text if current_node_name == tag_name_for_check: sub.subscribe_data_change(ch) if interest_node[int_node].get( "uplink_converter" ) is None: if interest_node[ int_node].get( 'converter' ) is None: converter = OpcUaUplinkConverter( interest_node[ int_node]) else: converter = TBUtility.check_and_import( self. __connector_type, interest_node[ int_node] ['converter']) interest_node[int_node][ "uplink_converter"] = converter else: converter = interest_node[ int_node][ "uplink_converter"] self.subscribed[ch] = { "converter": converter, "path": current_var_path } else: return except BadWaitingForInitialData: pass elif not self.__interest_nodes: log.error( "Nodes in mapping not found, check your settings.") except Exception as e: log.exception(e) @property def subscribed(self): return self._subscribed
class OPCUAServer(object): """ Each instance of this class manages a connection to its own OPC UA server. Methods are called to get node data from the server. """ def __init__(self, name, endPointAddress, nameSpaceUri=None, browseRootNodeIdentifier=None): # ---------- Setup ----------- self.name = name self.logger = logging.getLogger(self.name) self.endPointAddress = endPointAddress self.nameSpaceUri = nameSpaceUri self.nameSpaceIndex = None self.browseRootNodeIdentifier = browseRootNodeIdentifier self.rootNodeId = None self.client = Client(self.endPointAddress, timeout=2) self.sub = None self.subscriptions = {} # ---------------------------- def check_connection(self): """ Check if a connection has been established before or if connection thread is running. If either fails, try to (re)connect. """ if self.client.uaclient._uasocket is None: self.connect() elif self.client.uaclient._uasocket._thread is None: self.connect() elif not self.client.uaclient._uasocket._thread.is_alive(): self.connect() def connect(self): """ Connect to OPC UA server. If fails clean up session and socket, and raise exception. """ try: self.logger.info("Connecting to " + self.name + ".") self.client.connect() self.update_namespace_and_root_node_id() except socket.timeout: self.logger.info(self.name + " socket timed out.") try: self.logger.info("Cleaning up session and socket.") self.client.uaclient.disconnect() except AttributeError: pass self.logger.info("Socket and session cleaned up.") raise TimeoutError(self.name + " timed out.") def update_namespace_and_root_node_id(self): """ Update rootNodeId and nameSpaceIndex. If no namespace given, sets root node (id: i=84) as root node. """ if self.nameSpaceUri and self.browseRootNodeIdentifier: nsArray = self.client.get_namespace_array() index = nsArray.index(self.nameSpaceUri) if index > 0: nodeId = "ns={};".format(index) + self.browseRootNodeIdentifier else: nodeId = self.browseRootNodeIdentifier else: nodeId = "i=84" index = None self.rootNodeId = nodeId self.nameSpaceIndex = index return def get_node_path(self, nodeId): """ Create node path from node id for current server settings. Attempts to create a path of node from rootNode. Only works for folderly like string node ids. Example: "ns=2;s=node1.node2.node3.node4" """ identifierType = self.rootNodeId.split(";")[-1].split("=")[0] if (identifierType == "s") and ("." in self.rootNodeId): rootNodeName = self.rootNodeId.lower().split(".")[-1] nodePath = nodeId.lower().split(rootNodeName)[-1] nodePath = nodePath.replace(".", "", 1).replace(".", "/") else: nodePath = nodeId.split("=")[-1] return nodePath def get_node(self, nodeId=""): """ Returns node from nodeId or identifier. If no namespace given in nodeId, assumes the namespace to namespace given for the server in settings.py. Only the ns set for the server in servers.json is accessible via browsing. """ self.check_connection() if nodeId == "": nodeId = self.rootNodeId elif self.nameSpaceIndex is None: nodeId = nodeId elif nodeId[:3] == "ns=": identifier = nodeId.split(";")[-1] if self.nameSpaceIndex == 0: nodeId = identifier else: nodeId = f"ns={self.nameSpaceIndex};{identifier}" else: nodeId = f"ns={self.nameSpaceIndex};{nodeId}" return self.client.get_node(nodeId) async def get_variable_nodes(self, node, nodeClass=2, variableList=None, depth=0, maxDepth=10): """ Eats a list of node object(s). Recursively finds nodes under given nodes that have given nodeClass. Returns node objects in a list. """ if variableList is None: variableList = [] depth += 1 if depth >= maxDepth: return variableList nodes = node.get_children() params = ua.ReadParameters() for node in nodes: rv = ua.ReadValueId() rv.NodeId = node.nodeid rv.AttributeId = ua.AttributeIds.NodeClass params.NodesToRead.append(rv) results = [] if len(params.NodesToRead) > 0: results, readTime = await self.read(params) for i in range(len(results)): if nodeClass == results[i].Value.Value: variableList.append(nodes[i]) await self.get_variable_nodes(node=nodes[i], nodeClass=nodeClass, variableList=variableList, depth=depth) return variableList def subscribe_variable(self, nodeId): if self.sub is None: handler = self self.sub = self.client.create_subscription(100, handler) node = self.get_node(nodeId) if 2 == node.get_attribute(ua.AttributeIds.NodeClass).Value.Value: return self.sub.subscribe_data_change(node) else: return None def datachange_notification(self, node, value, data): self.subscriptions[node.nodeid.to_string()] = data.monitored_item.Value async def read_node_attribute(self, nodeId, attribute): """ Read node attribute based on given arguments. Giving correct dataType for value and node speeds up the write operation. Arguments Example nodeId: Target nodeId "ns=2;i=2" attribute: Target attribute of node "Value" Results OPCUAVar: OPC UA variable object <object> readTime: Time taken for read (ns) 12345678 """ rv = ua.ReadValueId() if nodeId == "": rv.NodeId = ua.NodeId.from_string(server.rootNodeId) else: rv.NodeId = ua.NodeId.from_string(nodeId) rv.AttributeId = ua.AttributeIds[attribute] params = ua.ReadParameters() params.NodesToRead.append(rv) result, readTime = await self.read(params) if attribute == "Value": return result[0], readTime else: return result[0] async def set_node_attribute(self, nodeId, attribute, value, dataType=None): """ Sets node attribute based on given arguments. Giving correct dataType for value and node speeds up the write operation. Arguments Example nodeId: Target nodeId "ns=2;i=2" attribute: Target attribute of node "Value" value: Value for the attribute 1234 dataType: Data type of value "Int32" Results boolean: Indicates success True writeTime: Time taken for write (ns) 12345678 """ attr = ua.WriteValue() if nodeId == "": attr.NodeId = ua.NodeId.from_string(self.rootNodeId) else: attr.NodeId = ua.NodeId.from_string(nodeId) attr.AttributeId = ua.AttributeIds[attribute] if attribute == "Description": dataValue = ua.LocalizedText(value) else: if dataType is None: variantType = self.variant_type_finder(value, nodeId) else: variantType = ua.VariantType[dataType] dataValue = ua.Variant(value, variantType) attr.Value = ua.DataValue(dataValue) params = ua.WriteParameters() params.NodesToWrite.append(attr) result, writeTime = await self.write(params) if attribute == "Value": return result[0].is_good(), writeTime else: return result[0].is_good() def add_node(self, name, nodeId, parentId, value=None, writable=True): """ Adds a node to OPC UA server. If value given, adds a variable node, else, a folder node. Requires server admin powers in server servers.json, for example endPointAddress: "opc.tcp://[email protected]:4840/freeopcua/server/". """ self.check_connection() if self.nameSpaceIndex is not None: index = self.nameSpaceIndex elif nodeId[:3] == "ns=": index = nodeId.split("=")[1][0] else: index = 0 browseName = f"{index}:{name}" parentNode = self.get_node(parentId) if value is None: node = parentNode.add_folder(nodeId, browseName) result = { "name": node.get_display_name().to_string(), "nodeId": node.nodeid.to_string(), } else: node = parentNode.add_variable(nodeId, browseName, value) attribute = node.get_attribute(ua.AttributeIds.Value) result = { "name": node.get_display_name().to_string(), "nodeId": node.nodeid.to_string(), "value": attribute.Value.Value, "dataType": attribute.Value.VariantType.name, "sourceTimestamp": attribute.SourceTimestamp, "statusCode": attribute.StatusCode.name } if writable is True: node.set_writable() return result def delete_node(self, nodeId, recursive=True): """ Recursively deletes node and it's subnodes unless recursive=False. Requires admins. Doesn't raise errors if deleting is unsuccessful. """ self.check_connection() node = self.get_node(nodeId) result = self.client.delete_nodes([node], recursive) result[1][0].check() return result[1][0].is_good() async def read(self, params): """ Reads from OPC UA server params == ua.ReadParameters() that are properly set up. Returns result object and time it took to read from OPC UA server. """ self.check_connection() start = time.time_ns() result = self.client.uaclient.read(params) readTime = time.time_ns() - start return result, readTime async def write(self, params): """ Writes to OPC UA server params == ua.WriteParameters() that are properly set up. Returns result object and time it took to read from OPC UA server. """ self.check_connection() start = time.time_ns() result = self.client.uaclient.write(params) writeTime = time.time_ns() - start return result, writeTime def variant_type_finder(self, value, nodeId): """ Attempts to find variant type of given value. If not found, retrieves variant type of node from OPC UA server. """ valueType = type(value) if isinstance(valueType, datetime.datetime): variantType = ua.uatypes.VariantType.DateTime elif valueType == bool: variantType = ua.uatypes.VariantType.Boolean elif valueType == str: variantType = ua.uatypes.VariantType.String elif valueType == int or valueType == float: node = self.get_node(nodeId) self.check_connection() variantType = node.get_data_type_as_variant_type() else: raise ValueError("Unsupported datatype") return variantType
class UaClientWrapper(object): def __init__(self, endpoint, targetNs): self.endpoint = endpoint self.client = Client(endpoint) try: self.client.connect() self.namespaceArray = self.client.get_namespace_array() self.targetNs = self.client.get_namespace_index(targetNs) print("Target NS %s" % targetNs) except: self.disconnect() def get_namespace_array(self): return self.namespaceArray def collect_child_nodes(self, node, tree): # iterate over all referenced nodes (31), only hierarchical references (33) for child in node.get_children(refs=33): if not tree.get("children"): tree["children"] = [] tree["children"].append({"node": child}) self.collect_child_nodes(child, tree["children"][-1]) def import_nodes(self, root_node=None): for ns in self.client.get_namespace_array(): self.nsMapping[self.client.get_namespace_index(ns)] = ns if root_node is None: root = self.client.get_root_node() else: root = self.client.get_node(root_node) tree = {} self.collect_child_nodes(root, tree) return tree def getNamespaceIndices(self, list): self.nsMapping = {} for ns in list: self.nsMapping[ns] = self.client.get_namespace_index(ns) return self.nsMapping def clearModel(self, rootNode): root = self.client.get_node(rootNode) self.client.delete_nodes(root.get_children(), True) def addObject(self, parentNodeId, bName, objectType, identifier=0): parent = self.client.get_node(parentNodeId) node = parent.add_object("ns=%i;i=%i" % (self.targetNs, identifier), bName, objectType) node.__class__ = Node return node def escapeName(self, name: str): return name.replace(":", "|") def addNodes(self, rootNode, objects): for obj in objects: if 'ua_object_type' in obj: if obj['value'] != '': name = obj['value'] else: name = obj['object'] node = self.addObject( rootNode, self.escapeName(name), "ns=%s;i=%s" % (self.nsMapping[obj['ua_object_type']['ns']], obj['ua_object_type']['i'])) obj['ua_node_id'] = node.nodeid.to_string() if 'objects' in obj: obj['objects'] = self.addNodes(obj['ua_node_id'], obj['objects']) elif 'ua_variable_qualified_name' in obj and obj['value'] != '': qname = ua.QualifiedName( obj['ua_variable_qualified_name']['browse_name'], self.nsMapping[obj['ua_variable_qualified_name']['ns']]) root = self.client.get_node(rootNode) variable = root.get_child(qname) variable.set_value(obj['value']) return obj def disconnect(self): self.client.disconnect()