mirror of
https://github.com/thingsboard/thingsboard-gateway
synced 2025-10-26 22:31:42 +08:00
Merge branch 'develop/2.4-python' of https://github.com/thingsboard/thingsboard-gateway into develop/2.4-python
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# 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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = thingsboard-gateway
|
||||
version = 2.2.4.2
|
||||
version = 2.2.5
|
||||
description = Thingsboard Gateway for IoT devices.
|
||||
long_description= file: README.md
|
||||
license = Apache Software License (Apache Software License 2.0)
|
||||
|
||||
10
setup.py
10
setup.py
@@ -19,10 +19,11 @@ setup(
|
||||
packages=['thingsboard_gateway', 'thingsboard_gateway.gateway', 'thingsboard_gateway.storage',
|
||||
'thingsboard_gateway.tb_client', 'thingsboard_gateway.connectors', 'thingsboard_gateway.connectors.ble',
|
||||
'thingsboard_gateway.connectors.mqtt', 'thingsboard_gateway.connectors.opcua', 'thingsboard_gateway.connectors.request',
|
||||
'thingsboard_gateway.connectors.modbus', 'thingsboard_gateway.connectors.can', 'thingsboard_gateway.tb_utility', 'thingsboard_gateway.extensions',
|
||||
'thingsboard_gateway.connectors.modbus', 'thingsboard_gateway.connectors.can', 'thingsboard_gateway.connectors.bacnet',
|
||||
'thingsboard_gateway.connectors.bacnet.bacnet_utilities', 'thingsboard_gateway.tb_utility', 'thingsboard_gateway.extensions',
|
||||
'thingsboard_gateway.extensions.mqtt', 'thingsboard_gateway.extensions.modbus', 'thingsboard_gateway.extensions.opcua',
|
||||
'thingsboard_gateway.extensions.ble', 'thingsboard_gateway.extensions.serial', 'thingsboard_gateway.extensions.request',
|
||||
'thingsboard_gateway.extensions.can'
|
||||
'thingsboard_gateway.extensions.can', 'thingsboard_gateway.extensions.bacnet'
|
||||
],
|
||||
install_requires=[
|
||||
'cffi',
|
||||
@@ -40,9 +41,10 @@ setup(
|
||||
'simplejson',
|
||||
'pyrsistent',
|
||||
'requests',
|
||||
'python-can'
|
||||
'python-can',
|
||||
'bacpypes>=0.18.0'
|
||||
],
|
||||
download_url='https://github.com/thingsboard/thingsboard-gateway/archive/2.2.4.2.tar.gz',
|
||||
download_url='https://github.com/thingsboard/thingsboard-gateway/archive/2.2.5.tar.gz',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'thingsboard-gateway = thingsboard_gateway.tb_gateway:daemon'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%define name thingsboard-gateway
|
||||
%define version 2.2.4.2
|
||||
%define unmangled_version 2.2.4.2
|
||||
%define version 2.2.5
|
||||
%define unmangled_version 2.2.5
|
||||
%define release 1
|
||||
|
||||
Summary: Thingsboard Gateway for IoT devices.
|
||||
|
||||
58
thingsboard_gateway/config/bacnet.json
Normal file
58
thingsboard_gateway/config/bacnet.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"general": {
|
||||
"objectName": "TB_gateway",
|
||||
"address": "192.168.188.181:1052",
|
||||
"objectIdentifier": 599,
|
||||
"maxApduLengthAccepted": 1024,
|
||||
"segmentationSupported": "segmentedBoth",
|
||||
"vendorIdentifier": 15
|
||||
},
|
||||
"devices": [
|
||||
{
|
||||
"deviceName": "BACnet Device ${objectName}",
|
||||
"deviceType": "default",
|
||||
"address": "192.168.188.181:10520",
|
||||
"pollPeriod": 10000,
|
||||
"attributes": [
|
||||
{
|
||||
"key": "temperature",
|
||||
"type": "string",
|
||||
"objectId": "analogOutput:1",
|
||||
"propertyId": "presentValue"
|
||||
}
|
||||
],
|
||||
"timeseries": [
|
||||
{
|
||||
"key": "state",
|
||||
"type": "bool",
|
||||
"objectId": "binaryValue:1",
|
||||
"propertyId": "presentValue"
|
||||
}
|
||||
],
|
||||
"attributeUpdates": [
|
||||
{
|
||||
"key": "brightness",
|
||||
"requestType": "writeProperty",
|
||||
"objectId": "analogOutput:1",
|
||||
"propertyId": "presentValue"
|
||||
}
|
||||
],
|
||||
"serverSideRpc": [
|
||||
{
|
||||
"method": "set_state",
|
||||
"requestType": "writeProperty",
|
||||
"requestTimeout": 10000,
|
||||
"objectId": "binaryOutput:1",
|
||||
"propertyId": "presentValue"
|
||||
},
|
||||
{
|
||||
"method": "get_state",
|
||||
"requestType": "readProperty",
|
||||
"requestTimeout": 10000,
|
||||
"objectId": "binaryOutput:1",
|
||||
"propertyId": "presentValue"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -41,35 +41,38 @@
|
||||
"address": 2
|
||||
}
|
||||
],
|
||||
"rpc": {
|
||||
"turnLightOn": {
|
||||
"rpc": [
|
||||
{
|
||||
"tag": "turnLightOn",
|
||||
"address": 4,
|
||||
"tag": "bit",
|
||||
"type": "bit",
|
||||
"bit": 2,
|
||||
"value": true
|
||||
},
|
||||
"turnLightOff": {
|
||||
{
|
||||
"tag": "turnLightOff",
|
||||
"address": 4,
|
||||
"tag": "bit",
|
||||
"type": "bit",
|
||||
"bit": 2,
|
||||
"value": false
|
||||
},
|
||||
"setCPUFanSpeed": {
|
||||
{
|
||||
"tag": "setCPUFanSpeed",
|
||||
"type": "int",
|
||||
"value": 42,
|
||||
"functionCode": 16,
|
||||
"address": 1,
|
||||
"byteOrder": "BIG",
|
||||
"registerCount": 2
|
||||
},
|
||||
"getCPULoad": {
|
||||
{
|
||||
"tag":"getCPULoad",
|
||||
"type": "int",
|
||||
"functionCode": 4,
|
||||
"address": 0,
|
||||
"byteOrder": "BIG",
|
||||
"registerCount": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
79
thingsboard_gateway/config/rest.json
Normal file
79
thingsboard_gateway/config/rest.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"host": "127.0.0.1",
|
||||
"port": "5000",
|
||||
"mapping":[
|
||||
{
|
||||
"endpoint": "/test_device",
|
||||
"HTTPMethod": [
|
||||
"POST"
|
||||
],
|
||||
"security":
|
||||
{
|
||||
"type": "basic",
|
||||
"username": "user",
|
||||
"password": "passwd"
|
||||
},
|
||||
"converter": {
|
||||
"type": "json",
|
||||
"deviceNameExpression": "Device ${name}",
|
||||
"deviceTypeExpression": "default",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "string",
|
||||
"key": "model",
|
||||
"value": "${sensorModel}"
|
||||
}
|
||||
],
|
||||
"timeseries": [
|
||||
{
|
||||
"type": "double",
|
||||
"key": "temperature",
|
||||
"value": "${temp}"
|
||||
},
|
||||
{
|
||||
"type": "double",
|
||||
"key": "humidity",
|
||||
"value": "${hum}",
|
||||
"converter": "CustomConverter"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"endpoint": "/test",
|
||||
"HTTPMethod": [
|
||||
"GET",
|
||||
"POST"
|
||||
],
|
||||
"security":
|
||||
{
|
||||
"type": "anonymous"
|
||||
},
|
||||
"converter": {
|
||||
"type": "custom",
|
||||
"class": "CustomConverter",
|
||||
"deviceNameExpression": "Device 2",
|
||||
"deviceTypeExpression": "default",
|
||||
"attributes": [
|
||||
{
|
||||
"type": "string",
|
||||
"key": "model",
|
||||
"value": "Model2"
|
||||
}
|
||||
],
|
||||
"timeseries": [
|
||||
{
|
||||
"type": "double",
|
||||
"key": "temperature",
|
||||
"value": "${temp}"
|
||||
},
|
||||
{
|
||||
"type": "double",
|
||||
"key": "humidity",
|
||||
"value": "${hum}"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -51,6 +51,11 @@ connectors:
|
||||
# configuration: can.json
|
||||
#
|
||||
# -
|
||||
# name: BACnet Connector
|
||||
# type: bacnet
|
||||
# configuration: bacnet.json
|
||||
#
|
||||
# -
|
||||
# name: Custom Serial Connector
|
||||
# type: serial
|
||||
# configuration: custom_serial.json
|
||||
|
||||
14
thingsboard_gateway/connectors/bacnet/__init__.py
Normal file
14
thingsboard_gateway/connectors/bacnet/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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.
|
||||
|
||||
267
thingsboard_gateway/connectors/bacnet/bacnet_connector.py
Normal file
267
thingsboard_gateway/connectors/bacnet/bacnet_connector.py
Normal file
@@ -0,0 +1,267 @@
|
||||
# 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.
|
||||
|
||||
from copy import deepcopy
|
||||
from random import choice
|
||||
from threading import Thread
|
||||
from time import time, sleep
|
||||
from string import ascii_lowercase
|
||||
from bacpypes.core import run, stop
|
||||
from bacpypes.pdu import Address, GlobalBroadcast, LocalBroadcast, LocalStation, RemoteStation
|
||||
from thingsboard_gateway.connectors.connector import Connector, log
|
||||
from thingsboard_gateway.connectors.bacnet.bacnet_utilities.tb_gateway_bacnet_application import TBBACnetApplication
|
||||
from thingsboard_gateway.tb_utility.tb_utility import TBUtility
|
||||
|
||||
|
||||
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._application = TBBACnetApplication(self, self.__config)
|
||||
self.__bacnet_core_thread = Thread(target=run, name="BACnet core thread")
|
||||
self.__bacnet_core_thread.start()
|
||||
self.__stopped = False
|
||||
self.__config_devices = self.__config["devices"]
|
||||
self.default_converters = {"uplink_converter": TBUtility.check_and_import(self.__connector_type, "BACnetUplinkConverter"),
|
||||
"downlink_converter": TBUtility.check_and_import(self.__connector_type, "BACnetDownlinkConverter")}
|
||||
self.__request_functions = {"writeProperty": self._application.do_write_property,
|
||||
"readProperty": self._application.do_read_property}
|
||||
self.__available_object_resources = {}
|
||||
self.rpc_requests_in_progress = {}
|
||||
self.__connected = False
|
||||
self.daemon = True
|
||||
|
||||
def open(self):
|
||||
self.__stopped = False
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
self.__connected = True
|
||||
self.scan_network()
|
||||
self._application.do_whois()
|
||||
log.debug("WhoIsRequest has been sent.")
|
||||
self.scan_network()
|
||||
while not self.__stopped:
|
||||
sleep(.1)
|
||||
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(.1)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
def close(self):
|
||||
self.__stopped = True
|
||||
self.__connected = False
|
||||
stop()
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def is_connected(self):
|
||||
return self.__connected
|
||||
|
||||
def on_attributes_update(self, content):
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
log.exception(e)
|
||||
|
||||
def server_side_rpc_handler(self, content):
|
||||
try:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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
|
||||
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:
|
||||
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)
|
||||
log.debug(iocb.ioError)
|
||||
else:
|
||||
log.error("Received unknown RPC response callback from device: %r", iocb)
|
||||
|
||||
|
||||
|
||||
def __rpc_cancel_processing(self, iocb):
|
||||
log.info("RPC with iocb %r - cancelled.", iocb)
|
||||
|
||||
def scan_network(self):
|
||||
self._application.do_whois()
|
||||
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]:
|
||||
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:
|
||||
log.exception(e)
|
||||
|
||||
def __bacnet_device_mapping_response_cb(self, iocb, callback_params):
|
||||
converter = callback_params["config"]["uplink_converter"]
|
||||
mapping_type = callback_params["mapping_type"]
|
||||
config = callback_params["config"]
|
||||
converted_data = {}
|
||||
try:
|
||||
converted_data = converter.convert((mapping_type, config), iocb.ioResponse if iocb.ioResponse else iocb.ioError)
|
||||
except Exception as e:
|
||||
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 TBUtility.check_and_import(self.__connector_type, device.get("class"))
|
||||
datatype_config[converter_type] = converter_object(device)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
def add_device(self, data):
|
||||
if self.__devices_address_name.get(data["address"]) is None:
|
||||
for device in self.__config_devices:
|
||||
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.pop("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)
|
||||
|
||||
log.debug(data["address"].addrType)
|
||||
except Exception as e:
|
||||
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
|
||||
24
thingsboard_gateway/connectors/bacnet/bacnet_converter.py
Normal file
24
thingsboard_gateway/connectors/bacnet/bacnet_converter.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# 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.
|
||||
|
||||
from thingsboard_gateway.connectors.converter import Converter, log
|
||||
|
||||
|
||||
class BACnetConverter:
|
||||
def __init__(self, config):
|
||||
pass
|
||||
|
||||
def convert(self, config, data):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
from thingsboard_gateway.connectors.bacnet.bacnet_converter import Converter, log
|
||||
|
||||
class BACnetDownlinkConverter(Converter):
|
||||
def __init__(self, config):
|
||||
self.__config = config
|
||||
|
||||
def convert(self, config, data):
|
||||
log.debug(config, data)
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
from bacpypes.apdu import APDU, ReadPropertyACK
|
||||
from bacpypes.primitivedata import Tag
|
||||
from bacpypes.constructeddata import ArrayOf
|
||||
from thingsboard_gateway.connectors.bacnet.bacnet_converter import BACnetConverter, log
|
||||
from thingsboard_gateway.tb_utility.tb_utility import TBUtility
|
||||
|
||||
|
||||
class BACnetUplinkConverter(BACnetConverter):
|
||||
def __init__(self, config):
|
||||
self.__config = config
|
||||
|
||||
def convert(self, config, data):
|
||||
value = None
|
||||
if isinstance(data, ReadPropertyACK):
|
||||
value = self.__property_value_from_apdu(data)
|
||||
if config is not None:
|
||||
datatypes = {"attributes": "attributes",
|
||||
"timeseries": "telemetry",
|
||||
"telemetry": "telemetry"}
|
||||
dict_result = {"deviceName": None, "deviceType": None, "attributes": [], "telemetry": []}
|
||||
dict_result["deviceName"] = self.__config.get("deviceName", config[1].get("name", "BACnet device"))
|
||||
dict_result["deviceType"] = self.__config.get("deviceType", "default")
|
||||
dict_result[datatypes[config[0]]].append({config[1]["key"]: value})
|
||||
else:
|
||||
dict_result = value
|
||||
log.debug("%r %r", self, dict_result)
|
||||
return dict_result
|
||||
|
||||
@staticmethod
|
||||
def __property_value_from_apdu(apdu: APDU):
|
||||
tag_list = apdu.propertyValue.tagList
|
||||
non_app_tags = [tag for tag in tag_list if tag.tagClass != Tag.applicationTagClass]
|
||||
if non_app_tags:
|
||||
raise RuntimeError("Value has some non-application tags")
|
||||
first_tag = tag_list[0]
|
||||
other_type_tags = [tag for tag in tag_list[1:] if tag.tagNumber != first_tag.tagNumber]
|
||||
if other_type_tags:
|
||||
raise RuntimeError("All tags must be the same type")
|
||||
datatype = Tag._app_tag_class[first_tag.tagNumber]
|
||||
if not datatype:
|
||||
raise RuntimeError("unknown datatype")
|
||||
if len(tag_list) > 1:
|
||||
datatype = ArrayOf(datatype)
|
||||
value = apdu.propertyValue.cast_out(datatype)
|
||||
return value
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# 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.
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
# 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.
|
||||
|
||||
from bacpypes.pdu import Address, GlobalBroadcast
|
||||
from bacpypes.object import get_datatype
|
||||
from bacpypes.apdu import APDU
|
||||
|
||||
from bacpypes.app import BIPSimpleApplication
|
||||
|
||||
from bacpypes.core import run, deferred, enable_sleeping
|
||||
from bacpypes.iocb import IOCB
|
||||
|
||||
from bacpypes.apdu import ReadPropertyRequest, WhoIsRequest, IAmRequest, WritePropertyRequest, SimpleAckPDU, ReadPropertyACK
|
||||
from bacpypes.primitivedata import Null, ObjectIdentifier
|
||||
from bacpypes.constructeddata import Any
|
||||
|
||||
from thingsboard_gateway.connectors.connector import log
|
||||
from thingsboard_gateway.tb_utility.tb_utility import TBUtility
|
||||
from thingsboard_gateway.connectors.bacnet.bacnet_utilities.tb_gateway_bacnet_device import TBBACnetDevice
|
||||
|
||||
|
||||
class TBBACnetApplication(BIPSimpleApplication):
|
||||
def __init__(self, connector, configuration):
|
||||
try:
|
||||
self.__config = configuration
|
||||
self.__connector = connector
|
||||
assert self.__config is not None
|
||||
assert self.__config.get("general") is not None
|
||||
self.requests_in_progress = {}
|
||||
self.discovered_devices = {}
|
||||
self.__device = TBBACnetDevice(self.__config["general"])
|
||||
super().__init__(self.__device, self.__config["general"]["address"])
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
def do_whois(self, device=None):
|
||||
try:
|
||||
device = {} if device is None else device
|
||||
request = WhoIsRequest()
|
||||
address = device.get("address")
|
||||
low_limit = device.get("idLowLimit")
|
||||
high_limit = device.get("idHighLimit")
|
||||
if address is not None:
|
||||
request.pduDestination = Address(address)
|
||||
else:
|
||||
request.pduDestination = GlobalBroadcast()
|
||||
if low_limit is not None and high_limit is not None:
|
||||
request.deviceInstanceRangeLowLimit = int(low_limit)
|
||||
request.deviceInstanceRangeHighLimit = int(high_limit)
|
||||
iocb = IOCB(request)
|
||||
deferred(self.request_io, iocb)
|
||||
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
def indication(self, apdu: APDU):
|
||||
if isinstance(apdu, IAmRequest):
|
||||
log.debug("Received IAmRequest from device with ID: %i and address %s:%i",
|
||||
apdu.iAmDeviceIdentifier[1],
|
||||
apdu.pduSource.addrTuple[0],
|
||||
apdu.pduSource.addrTuple[1]
|
||||
)
|
||||
log.debug(apdu.pduSource)
|
||||
request = ReadPropertyRequest(
|
||||
destination=apdu.pduSource,
|
||||
objectIdentifier=apdu.iAmDeviceIdentifier,
|
||||
propertyIdentifier='objectName',
|
||||
)
|
||||
iocb = IOCB(request)
|
||||
deferred(self.request_io, iocb)
|
||||
iocb.add_callback(self.__iam_cb, vendor_id=apdu.vendorID)
|
||||
self.requests_in_progress.update({iocb: {"callback": self.__iam_cb}})
|
||||
|
||||
def do_read_property(self, device, mapping_type=None, config=None, callback=None):
|
||||
try:
|
||||
iocb = device if isinstance(device, IOCB) else self.form_iocb(device, config, "readProperty")
|
||||
deferred(self.request_io, iocb)
|
||||
self.requests_in_progress.update({iocb: {"callback": callback,
|
||||
"device": device,
|
||||
"mapping_type": mapping_type,
|
||||
"config": config}})
|
||||
iocb.add_callback(self.__general_cb)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
def do_write_property(self, device, callback=None):
|
||||
try:
|
||||
iocb = device if isinstance(device, IOCB) else self.form_iocb(device, request_type="writeProperty")
|
||||
deferred(self.request_io, iocb)
|
||||
self.requests_in_progress.update({iocb: {"callback": callback}})
|
||||
iocb.add_callback(self.__general_cb)
|
||||
except Exception as error:
|
||||
log.exception("exception: %r", error)
|
||||
|
||||
def check_or_add(self, device):
|
||||
device_address = device["address"] if isinstance(device["address"], Address) else Address(device["address"])
|
||||
if self.discovered_devices.get(device_address) is None:
|
||||
self.do_whois(device)
|
||||
return False
|
||||
return True
|
||||
|
||||
def __on_mapping_response_cb(self, iocb: IOCB):
|
||||
try:
|
||||
if self.requests_in_progress.get(iocb) is not None:
|
||||
log.debug(iocb)
|
||||
log.debug(self.requests_in_progress[iocb])
|
||||
if iocb.ioResponse:
|
||||
apdu = iocb.ioResponse
|
||||
value = self.__property_value_from_apdu(apdu)
|
||||
callback_params = self.requests_in_progress[iocb]
|
||||
if callback_params.get("callback") is not None:
|
||||
self.__general_cb(iocb, callback_params, value)
|
||||
elif iocb.ioError:
|
||||
log.exception(iocb.ioError)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
if self.requests_in_progress.get(iocb) is not None:
|
||||
del self.requests_in_progress[iocb]
|
||||
|
||||
def __general_cb(self, iocb, callback_params=None, value=None):
|
||||
try:
|
||||
if callback_params is None:
|
||||
callback_params = self.requests_in_progress[iocb]
|
||||
if iocb.ioResponse:
|
||||
apdu = iocb.ioResponse
|
||||
if isinstance(apdu, SimpleAckPDU):
|
||||
log.debug("Write to %s - successfully.", str(apdu.pduSource))
|
||||
else:
|
||||
log.debug("Received response: %r", apdu)
|
||||
elif iocb.ioError:
|
||||
log.exception(iocb.ioError)
|
||||
else:
|
||||
log.error("There are no data in response and no errors.")
|
||||
if isinstance(callback_params, dict) and callback_params.get("callback"):
|
||||
try:
|
||||
callback_params["callback"](iocb, callback_params)
|
||||
except TypeError:
|
||||
callback_params["callback"](iocb)
|
||||
except Exception as e:
|
||||
log.exception("During processing callback, exception has been raised:")
|
||||
log.exception(e)
|
||||
if self.requests_in_progress.get(iocb) is not None:
|
||||
del self.requests_in_progress[iocb]
|
||||
|
||||
def __iam_cb(self, iocb: IOCB, vendor_id=None):
|
||||
if iocb.ioResponse:
|
||||
apdu = iocb.ioResponse
|
||||
log.debug("Received IAm Response: %s", str(apdu))
|
||||
if self.discovered_devices.get(apdu.pduSource) is None:
|
||||
self.discovered_devices[apdu.pduSource] = {}
|
||||
value = self.__connector.default_converters["uplink_converter"]("{}").convert(None, apdu)
|
||||
log.debug("Property: %s is %s", apdu.propertyIdentifier, value)
|
||||
self.discovered_devices[apdu.pduSource].update({apdu.propertyIdentifier: value})
|
||||
data_to_connector = {
|
||||
"address": apdu.pduSource,
|
||||
"objectId": apdu.objectIdentifier,
|
||||
"name": value,
|
||||
"vendor": vendor_id if vendor_id is not None else 0
|
||||
}
|
||||
self.__connector.add_device(data_to_connector)
|
||||
elif iocb.ioError:
|
||||
log.exception(iocb.ioError)
|
||||
|
||||
@staticmethod
|
||||
def form_iocb(device, config=None, request_type="readProperty"):
|
||||
config = config if config is not None else device
|
||||
address = device["address"] if isinstance(device["address"], Address) else Address(device["address"])
|
||||
object_id = ObjectIdentifier(config["objectId"])
|
||||
property_id = config.get("propertyId")
|
||||
value = config.get("propertyValue")
|
||||
property_index = config.get("propertyIndex")
|
||||
priority = config.get("priority")
|
||||
vendor = device.get("vendor", config.get("vendorId", 0))
|
||||
request = None
|
||||
iocb = None
|
||||
if request_type == "readProperty":
|
||||
try:
|
||||
request = ReadPropertyRequest(
|
||||
objectIdentifier=object_id,
|
||||
propertyIdentifier=property_id
|
||||
)
|
||||
request.pduDestination = address
|
||||
if property_index is not None:
|
||||
request.propertyArrayIndex = int(property_index)
|
||||
iocb = IOCB(request)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
elif request_type == "writeProperty":
|
||||
datatype = get_datatype(object_id.value[0], property_id, vendor)
|
||||
if (isinstance(value, str) and value.lower() == 'null') or value is None:
|
||||
value = Null()
|
||||
request = WritePropertyRequest(
|
||||
objectIdentifier=object_id,
|
||||
propertyIdentifier=property_id
|
||||
)
|
||||
request.pduDestination = address
|
||||
request.propertyValue = Any()
|
||||
try:
|
||||
value = datatype(value)
|
||||
request.propertyValue = Any(value)
|
||||
except AttributeError as e:
|
||||
log.debug(e)
|
||||
except Exception as error:
|
||||
log.exception("WriteProperty cast error: %r", error)
|
||||
if property_index is not None:
|
||||
request.propertyArrayIndex = property_index
|
||||
if priority is not None:
|
||||
request.priority = priority
|
||||
iocb = IOCB(request)
|
||||
else:
|
||||
log.error("Request type is not found or not implemented")
|
||||
return iocb
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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.
|
||||
|
||||
from bacpypes.local.device import LocalDeviceObject
|
||||
from thingsboard_gateway.connectors.connector import log
|
||||
|
||||
|
||||
class TBBACnetDevice(LocalDeviceObject):
|
||||
def __init__(self, configuration):
|
||||
assert configuration is not None
|
||||
super().__init__(**configuration)
|
||||
@@ -211,8 +211,8 @@ class ModbusConnector(Connector, threading.Thread):
|
||||
2: self.__master.read_discrete_inputs,
|
||||
3: self.__master.read_holding_registers,
|
||||
4: self.__master.read_input_registers,
|
||||
5: self.__master.write_coils,
|
||||
6: self.__master.write_registers,
|
||||
5: self.__master.write_coil,
|
||||
6: self.__master.write_register,
|
||||
15: self.__master.write_coils,
|
||||
16: self.__master.write_registers,
|
||||
}
|
||||
|
||||
14
thingsboard_gateway/extensions/bacnet/__init__.py
Normal file
14
thingsboard_gateway/extensions/bacnet/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# 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.
|
||||
|
||||
@@ -74,7 +74,8 @@ class TBGatewayService:
|
||||
"opcua": "OpcUaConnector",
|
||||
"ble": "BLEConnector",
|
||||
"request": "RequestConnector",
|
||||
"can": "CanConnector"
|
||||
"can": "CanConnector",
|
||||
"bacnet": "BACnetConnector",
|
||||
}
|
||||
self._implemented_connectors = {}
|
||||
self._event_storage_types = {
|
||||
|
||||
Reference in New Issue
Block a user