1
0
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:
zbeacon
2020-04-23 08:16:41 +03:00
19 changed files with 842 additions and 19 deletions

View 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.

View File

@@ -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)

View File

@@ -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'

View File

@@ -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.

View 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"
}
]
}
]
}

View File

@@ -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
}
}
]
}
]
}

View 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}"
}
]
}
}
]
}

View File

@@ -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

View 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.

View 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

View 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

View File

@@ -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)

View File

@@ -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

View 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.

View File

@@ -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

View File

@@ -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)

View File

@@ -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,
}

View 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.

View File

@@ -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 = {