mirror of
https://github.com/thingsboard/thingsboard-gateway
synced 2025-10-26 22:31:42 +08:00
483 lines
26 KiB
Python
483 lines
26 KiB
Python
# Copyright 2020. ThingsBoard
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import re
|
|
import time
|
|
from concurrent.futures import TimeoutError as FuturesTimeoutError
|
|
from copy import deepcopy
|
|
from random import choice
|
|
from threading import Thread
|
|
from string import ascii_lowercase
|
|
import regex
|
|
from opcua import Client, ua
|
|
from simplejson import dumps
|
|
from thingsboard_gateway.tb_utility.tb_utility import TBUtility
|
|
from thingsboard_gateway.connectors.connector import Connector, log
|
|
from thingsboard_gateway.connectors.opcua.opcua_uplink_converter import OpcUaUplinkConverter
|
|
|
|
|
|
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 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.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.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 FuturesTimeoutError:
|
|
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 isinstance(arguments, 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 None
|
|
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=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)
|
|
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 = 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('\\\\', '\\')
|
|
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 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 = '\\\\.'.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 SubHandler(object):
|
|
def __init__(self, connector: OpcUaConnector):
|
|
self.connector = connector
|
|
|
|
def datachange_notification(self, node, val, data):
|
|
try:
|
|
log.debug("Python: New data change event on node %s, with val: %s and data %s", node, val, str(data))
|
|
subscription = self.connector.subscribed[node]
|
|
converted_data = subscription["converter"].convert((subscription["config_path"], subscription["path"]), val)
|
|
self.connector.statistics['MessagesReceived'] += 1
|
|
self.connector.data_to_send.append(converted_data)
|
|
self.connector.statistics['MessagesSent'] += 1
|
|
log.debug("Data to ThingsBoard: %s", converted_data)
|
|
except Exception as e:
|
|
log.exception(e)
|
|
|
|
def event_notification(self, event):
|
|
try:
|
|
log.debug("Python: New event %s", event)
|
|
except Exception as e:
|
|
log.exception(e)
|