1
0
mirror of https://github.com/thingsboard/thingsboard-gateway synced 2025-10-26 22:31:42 +08:00
Files
thingsboard-gateway/thingsboard_gateway/connectors/bacnet/bacnet_connector.py

336 lines
17 KiB
Python

# Copyright 2022. 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.
from queue import Queue
from random import choice
from string import ascii_lowercase
from threading import Thread
from time import sleep, time
from thingsboard_gateway.gateway.statistics_service import StatisticsService
from thingsboard_gateway.tb_utility.tb_loader import TBModuleLoader
from thingsboard_gateway.tb_utility.tb_utility import TBUtility
from thingsboard_gateway.tb_utility.tb_logger import init_logger
try:
from bacpypes.core import run, stop
except ImportError:
print("BACnet library not found - installing...")
TBUtility.install_package("bacpypes", ">=0.18.0")
from bacpypes.core import run, stop
from bacpypes.pdu import Address, GlobalBroadcast, LocalBroadcast, LocalStation, RemoteStation
from thingsboard_gateway.connectors.connector import Connector
from thingsboard_gateway.connectors.bacnet.bacnet_utilities.tb_gateway_bacnet_application import TBBACnetApplication
class BACnetConnector(Thread, Connector):
def __init__(self, gateway, config, connector_type):
self._connector_type = connector_type
self.statistics = {'MessagesReceived': 0,
'MessagesSent': 0}
super().__init__()
self.__config = config
self.setName(config.get('name', 'BACnet ' + ''.join(choice(ascii_lowercase) for _ in range(5))))
self.__devices = []
self.__device_indexes = {}
self.__devices_address_name = {}
self.__gateway = gateway
self._log = init_logger(self.__gateway, self.name, self.__config.get('logLevel', 'INFO'))
self._application = TBBACnetApplication(self, self.__config, self._log)
self.__bacnet_core_thread = Thread(target=run, name="BACnet core thread", daemon=True,
kwargs={"sigterm": None, "sigusr1": None})
self.__bacnet_core_thread.start()
self.__stopped = False
self.__config_devices = self.__config["devices"]
self.default_converters = {
"uplink_converter": TBModuleLoader.import_module(self._connector_type, "BACnetUplinkConverter"),
"downlink_converter": TBModuleLoader.import_module(self._connector_type, "BACnetDownlinkConverter")}
self.__request_functions = {"writeProperty": self._application.do_write_property,
"readProperty": self._application.do_read_property,
"risingEdge": self._application.do_binary_rising_edge}
self.__available_object_resources = {}
self.rpc_requests_in_progress = {}
self.__connected = False
self.daemon = True
self.__convert_and_save_data_queue = Queue()
def open(self):
self.__stopped = False
self.start()
def run(self):
self.__connected = True
self.scan_network()
self._application.do_whois()
self._log.debug("WhoIsRequest has been sent.")
self.scan_network()
while not self.__stopped:
sleep(.2)
for device in self.__devices:
try:
if device.get("previous_check") is None or time() * 1000 - device["previous_check"] >= device[
"poll_period"]:
for mapping_type in ["attributes", "telemetry"]:
for config in device[mapping_type]:
if config.get("uplink_converter") is None or config.get("downlink_converter") is None:
self.__load_converters(device)
data_to_application = {
"device": device,
"mapping_type": mapping_type,
"config": config,
"callback": self.__bacnet_device_mapping_response_cb
}
self._application.do_read_property(**data_to_application)
device["previous_check"] = time() * 1000
else:
sleep(.2)
except Exception as e:
self._log.exception(e)
if not self.__convert_and_save_data_queue.empty():
for _ in range(self.__convert_and_save_data_queue.qsize()):
thread = Thread(target=self.__convert_and_save_data, args=(self.__convert_and_save_data_queue,),
daemon=True)
thread.start()
def close(self):
self.__stopped = True
self.__connected = False
self._log.reset()
self._application.mux.directPort.connected = False
self._application.mux.directPort.accepting = False
self._application.mux.directPort.connecting = False
self._application.mux.directPort.del_channel()
if self._application.mux.directPort.socket is not None:
try:
sleep(1)
self._application.mux.directPort.socket.close()
except OSError:
pass
stop()
self._log.info('BACnet connector has been stopped.')
def get_name(self):
return self.name
def get_type(self):
return self._connector_type
def is_connected(self):
return self.__connected
@StatisticsService.CollectAllReceivedBytesStatistics(start_stat_type='allReceivedBytesFromTB')
def on_attributes_update(self, content):
try:
self._log.debug('Recieved Attribute Update Request: %r', str(content))
for device in self.__devices:
if device["deviceName"] == content["device"]:
for request in device["attribute_updates"]:
if request["config"].get("requestType") is not None:
for attribute in content["data"]:
if attribute == request["key"]:
request["iocb"][1]["config"].update({"propertyValue": content["data"][attribute]})
kwargs = request["iocb"][1]
iocb = request["iocb"][0](device, **kwargs)
self.__request_functions[request["config"]["requestType"]](iocb)
return
else:
self._log.error("\"requestType\" not found in request configuration for key %s device: %s",
request.get("key", "[KEY IS EMPTY]"),
device["deviceName"])
except Exception as e:
self._log.exception(e)
@StatisticsService.CollectAllReceivedBytesStatistics(start_stat_type='allReceivedBytesFromTB')
def server_side_rpc_handler(self, content):
try:
self._log.debug('Recieved RPC Request: %r', str(content))
for device in self.__devices:
if device["deviceName"] == content["device"]:
method_found = False
for request in device["server_side_rpc"]:
if request["config"].get("requestType") is not None:
if content["data"]["method"] == request["method"]:
method_found = True
kwargs = request["iocb"][1]
timeout = time() * 1000 + request["config"].get("requestTimeout", 200)
if content["data"].get("params") is not None:
kwargs["config"].update({"propertyValue": content["data"]["params"]})
iocb = request["iocb"][0](device, **kwargs)
self.__request_functions[request["config"]["requestType"]](device=iocb,
callback=self.__rpc_response_cb)
self.rpc_requests_in_progress[iocb] = {"content": content,
"uplink_converter": request["uplink_converter"]}
# self.__gateway.register_rpc_request_timeout(content,
# timeout,
# iocb,
# self.__rpc_cancel_processing)
else:
self._log.error("\"requestType\" not found in request configuration for key %s device: %s",
request.get("key", "[KEY IS EMPTY]"),
device["deviceName"])
if not method_found:
self._log.error("RPC method %s not found in configuration", content["data"]["method"])
self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], success_sent=False)
except Exception as e:
self._log.exception(e)
def __rpc_response_cb(self, iocb, callback_params=None):
device = self.rpc_requests_in_progress[iocb]
converter = device["uplink_converter"]
content = device["content"]
if iocb.ioResponse:
apdu = iocb.ioResponse
self._log.debug("Received callback with Response: %r", apdu)
converted_data = converter.convert(None, apdu)
if converted_data is None:
converted_data = {"success": True}
self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], converted_data)
# self.__gateway.rpc_with_reply_processing(iocb, converted_data or {"success": True})
elif iocb.ioError:
self._log.exception("Received callback with Error: %r", iocb.ioError)
data = {"error": str(iocb.ioError)}
self.__gateway.send_rpc_reply(content["device"], content["data"]["id"], data)
self._log.debug(iocb.ioError)
else:
self._log.error("Received unknown RPC response callback from device: %r", iocb)
def __rpc_cancel_processing(self, iocb):
self._log.info("RPC with iocb %r - cancelled.", iocb)
def scan_network(self):
self._application.do_whois()
self._log.debug("WhoIsRequest has been sent.")
for device in self.__config_devices:
try:
if self._application.check_or_add(device):
for mapping_type in ["attributes", "timeseries"]:
for config in device[mapping_type]:
if config.get("uplink_converter") is None or config.get("downlink_converter") is None:
self.__load_converters(device)
data_to_application = {
"device": device,
"mapping_type": mapping_type,
"config": config,
"callback": self.__bacnet_device_mapping_response_cb
}
self._application.do_read_property(**data_to_application)
except Exception as e:
self._log.exception(e)
def __convert_and_save_data(self, queue):
converter, mapping_type, config, iocb = queue.get()
converted_data = {}
try:
converted_data = converter.convert((mapping_type, config),
iocb.ioResponse if iocb.ioResponse else iocb.ioError)
except Exception as e:
self._log.exception(e)
self.__gateway.send_to_storage(self.name, converted_data)
def __bacnet_device_mapping_response_cb(self, iocb, callback_params):
mapping_type = callback_params["mapping_type"]
config = callback_params["config"]
converted_data = {}
converter = callback_params["config"].get("uplink_converter")
if converter is None:
for device in self.__devices:
self.__load_converters(device)
else:
converter = callback_params["config"].get("uplink_converter")
try:
converted_data = converter.convert((mapping_type, config),
iocb.ioResponse if iocb.ioResponse else iocb.ioError)
except Exception as e:
self._log.exception(e)
self.__gateway.send_to_storage(self.name, converted_data)
def __load_converters(self, device):
datatypes = ["attributes", "telemetry", "attribute_updates", "server_side_rpc"]
for datatype in datatypes:
for datatype_config in device.get(datatype, []):
try:
for converter_type in self.default_converters:
converter_object = self.default_converters[converter_type] if datatype_config.get(
"class") is None else TBModuleLoader.import_module(self._connector_type,
device.get("class"))
datatype_config[converter_type] = converter_object(device, self._log)
except Exception as e:
self._log.exception(e)
def add_device(self, data):
if self.__devices_address_name.get(data["address"]) is None:
for device in self.__config_devices:
if device["address"] == data["address"]:
try:
config_address = Address(device["address"])
device_name_tag = TBUtility.get_value(device["deviceName"], get_tag=True)
device_name = device["deviceName"].replace("${" + device_name_tag + "}", data["name"])
device_information = {
**data,
**self.__get_requests_configs(device),
"type": device["deviceType"],
"config": device,
"attributes": device.get("attributes", []),
"telemetry": device.get("timeseries", []),
"poll_period": device.get("pollPeriod", 5000),
"deviceName": device_name,
}
if config_address == data["address"] or \
(config_address, GlobalBroadcast) or \
(isinstance(config_address, LocalBroadcast) and isinstance(device["address"],
LocalStation)) or \
(isinstance(config_address, (LocalStation, RemoteStation)) and isinstance(
data["address"], (
LocalStation, RemoteStation))):
self.__devices_address_name[data["address"]] = device_information["deviceName"]
self.__devices.append(device_information)
self._log.debug(data["address"].addrType)
except Exception as e:
self._log.exception(e)
def __get_requests_configs(self, device):
result = {"attribute_updates": [], "server_side_rpc": []}
for request in device.get("attributeUpdates", []):
kwarg_dict = {
"config": request,
"request_type": request["requestType"]
}
request_config = {
"key": request["key"],
"iocb": (self._application.form_iocb, kwarg_dict),
"config": request
}
result["attribute_updates"].append(request_config)
for request in device.get("serverSideRpc", []):
kwarg_dict = {
"config": request,
"request_type": request["requestType"]
}
request_config = {
"method": request["method"],
"iocb": (self._application.form_iocb, kwarg_dict),
"config": request
}
result["server_side_rpc"].append(request_config)
return result
def get_config(self):
return self.__config