Skip to content

Commit e963962

Browse files
authored
Added unit tests for BACnet connector (#1914)
* implement base bacnet connector test * refactor on_attribute_update method inside bacnet connector * implement functional pattern for bacnet tests * implement basic tests for get_device_by_name method * refactor bacnet on_attribute_update connector * basic unit-tests for on_attribute_updates * add configuration for on_attribute_update tests * write-unit-tests for bacnet on-attribute-updates connector * refactor bacnet connector by adding more explicit error handling and logs * add more configs to test on_attribute_update bacnet * refactor reserved rpc methods for bacnet connector * add more robust handling on ValueError * implement unit-tests for reserved rpc * fix errors inside bacnet connector, add better log messages and more robust error validation * add license to tests * fix error with update attribute with rpc using readProperty requestType * add configs for bacnet device rpc tests * implement tests for device rpc * update bacnet on_attribute_update tests * add whitespaces at the end of each line * Revert "add whitespaces at the end of each line" This reverts commit a2f5f9c. * add whitespaces at the end of each line * remove unnecessary idents from bacnet connector * remove asyncio.run_coroutine_threadsafe, add custom task handling * fix bacnet-unit server side rpc tests * fix on_attributes_update bacnet tests
1 parent d5d9450 commit e963962

14 files changed

+1710
-87
lines changed
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2025. ThingsBoard
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
from asyncio import Queue, new_event_loop
17+
from os import path
18+
from unittest import IsolatedAsyncioTestCase
19+
from unittest.mock import MagicMock
20+
from bacpypes3.apdu import IAmRequest
21+
from bacpypes3.pdu import Address
22+
from simplejson import load
23+
from thingsboard_gateway.connectors.bacnet.device import Device, Devices
24+
from thingsboard_gateway.connectors.bacnet.bacnet_connector import AsyncBACnetConnector
25+
26+
27+
class BacnetBaseTestCase(IsolatedAsyncioTestCase):
28+
CONFIG_PATH = path.join(path.dirname(path.abspath(__file__)), 'data')
29+
DEVICE_NAME = 'test emulator device'
30+
CONNECTOR_TYPE = 'bacnet'
31+
32+
async def asyncSetUp(self):
33+
self.connector: AsyncBACnetConnector = AsyncBACnetConnector.__new__(AsyncBACnetConnector)
34+
self.connector._AsyncBACnetConnector__log = logging.getLogger('Bacnet test')
35+
self.connector.loop = new_event_loop()
36+
self.connector._AsyncBACnetConnector__process_device_queue = Queue(1_000_000)
37+
self.connector._AsyncBACnetConnector__process_device_rescan_queue = Queue(1_000_000)
38+
self.connector._AsyncBACnetConnector__devices = Devices()
39+
self.device = self.create_fake_device(
40+
attribute_update_config_path='attribute_updates/on_attribute_updates_bacnet_config.json')
41+
await self.connector._AsyncBACnetConnector__devices.add(self.device)
42+
if not hasattr(self.connector, '_AsyncBACnetConnector__gateway'):
43+
self.connector._AsyncBACnetConnector__gateway = MagicMock()
44+
45+
async def asyncTearDown(self):
46+
log = logging.getLogger('Bacnet test')
47+
for handler in list(log.handlers):
48+
log.removeHandler(handler)
49+
self.connector = None
50+
self.device = None
51+
await super().asyncTearDown()
52+
53+
@staticmethod
54+
def convert_json(config_path):
55+
with open(config_path, 'r') as config_file:
56+
config = load(config_file)
57+
return config
58+
59+
@staticmethod
60+
def create_fake_apdu_i_am_request(
61+
addr: str = "192.168.1.136:47809",
62+
device_id: int = 1234,
63+
device_name: str = "test emulator device",
64+
max_apdu: int = 1476,
65+
segmentation: str = "segmentedBoth",
66+
vendor_id: int = 15,
67+
):
68+
apdu = IAmRequest(
69+
iAmDeviceIdentifier=("device", device_id),
70+
maxAPDULengthAccepted=max_apdu,
71+
segmentationSupported=segmentation,
72+
vendorID=vendor_id,
73+
)
74+
apdu.pduSource = Address(addr)
75+
apdu.deviceName = device_name
76+
apdu.routerName = None
77+
apdu.routerAddress = None
78+
apdu.routerId = None
79+
apdu.routerVendorId = None
80+
81+
return apdu
82+
83+
def create_fake_device(self, attribute_update_config_path):
84+
device = Device(
85+
connector_type=self.CONNECTOR_TYPE,
86+
config=self.convert_json(path.join(self.CONFIG_PATH, attribute_update_config_path)),
87+
i_am_request=self.create_fake_apdu_i_am_request(),
88+
reading_queue=self.connector._AsyncBACnetConnector__process_device_queue,
89+
rescan_queue=self.connector._AsyncBACnetConnector__process_device_rescan_queue,
90+
logger=self.connector._AsyncBACnetConnector__log,
91+
converter_logger=MagicMock(),
92+
)
93+
return device

tests/unit/connectors/bacnet/bacnet_test_server_side_rpc.py

Lines changed: 387 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"altResponsesAddresses": [],
3+
"deviceInfo": {
4+
"deviceNameExpression": "test emulator device",
5+
"deviceNameExpressionSource": "expression",
6+
"deviceProfileExpressionSource": "constant",
7+
"deviceProfileExpression": "default"
8+
},
9+
"pollPeriod": 10000,
10+
"timeseries": [
11+
{
12+
"key": "power",
13+
"objectType": "analogValue",
14+
"objectId": 1,
15+
"propertyId": "presentValue"
16+
},
17+
{
18+
"key": "frequency",
19+
"objectType": "analogValue",
20+
"objectId": 2,
21+
"propertyId": "presentValue"
22+
},
23+
{
24+
"key": "temperature",
25+
"objectType": "analogOutput",
26+
"objectId": 1,
27+
"propertyId": "presentValue"
28+
},
29+
{
30+
"key": "presure",
31+
"objectType": "analogInput",
32+
"objectId": 2,
33+
"propertyId": "presentValue"
34+
}
35+
],
36+
"attributes": [
37+
{
38+
"key": "binaryInput1",
39+
"objectType": "binaryInput",
40+
"objectId": 1,
41+
"propertyId": "presentValue"
42+
},
43+
{
44+
"key": "binaryValue2",
45+
"objectType": "binaryValue",
46+
"objectId": 2,
47+
"propertyId": "presentValue"
48+
},
49+
{
50+
"key": "binaryOutput",
51+
"objectType": "binaryOutput",
52+
"objectId": 1,
53+
"propertyId": "presentValue"
54+
}
55+
],
56+
"attributeUpdates": [
57+
{
58+
"key": "binaryValue2",
59+
"objectType": "binaryValue",
60+
"objectId": 2,
61+
"propertyId": "presentValue"
62+
}
63+
],
64+
"serverSideRpc": [],
65+
"address": "192.168.1.136:47809",
66+
"devicesRescanObjectsPeriodSeconds": 60
67+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"altResponsesAddresses": [],
3+
"deviceInfo": {
4+
"deviceNameExpression": "test emulator device",
5+
"deviceNameExpressionSource": "expression",
6+
"deviceProfileExpressionSource": "constant",
7+
"deviceProfileExpression": "default"
8+
},
9+
"pollPeriod": 10000,
10+
"timeseries": [
11+
{
12+
"key": "power",
13+
"objectType": "analogValue",
14+
"objectId": 1,
15+
"propertyId": "presentValue"
16+
},
17+
{
18+
"key": "frequency",
19+
"objectType": "analogValue",
20+
"objectId": 2,
21+
"propertyId": "presentValue"
22+
},
23+
{
24+
"key": "temperature",
25+
"objectType": "analogOutput",
26+
"objectId": 1,
27+
"propertyId": "presentValue"
28+
},
29+
{
30+
"key": "presure",
31+
"objectType": "analogInput",
32+
"objectId": 2,
33+
"propertyId": "presentValue"
34+
}
35+
],
36+
"attributes": [
37+
{
38+
"key": "binaryInput1",
39+
"objectType": "binaryInput",
40+
"objectId": 1,
41+
"propertyId": "presentValue"
42+
},
43+
{
44+
"key": "binaryValue2",
45+
"objectType": "binaryValue",
46+
"objectId": 2,
47+
"propertyId": "presentValue"
48+
},
49+
{
50+
"key": "binaryOutput",
51+
"objectType": "binaryOutput",
52+
"objectId": 1,
53+
"propertyId": "presentValue"
54+
}
55+
],
56+
"attributeUpdates": [
57+
58+
],
59+
"serverSideRpc": [],
60+
"address": "192.168.1.136:47809",
61+
"devicesRescanObjectsPeriodSeconds": 60
62+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{
2+
"altResponsesAddresses": [],
3+
"host": "192.168.1.136",
4+
"port": 47809,
5+
"deviceInfo": {
6+
"deviceNameExpression": "test emulator device",
7+
"deviceNameExpressionSource": "expression",
8+
"deviceProfileExpressionSource": "constant",
9+
"deviceProfileExpression": "default"
10+
},
11+
"pollPeriod": 10000,
12+
"timeseries": [
13+
{
14+
"key": "power",
15+
"objectType": "analogValue",
16+
"objectId": 1,
17+
"propertyId": "presentValue"
18+
},
19+
{
20+
"key": "frequency",
21+
"objectType": "analogValue",
22+
"objectId": 2,
23+
"propertyId": "presentValue"
24+
},
25+
{
26+
"key": "temperature",
27+
"objectType": "analogOutput",
28+
"objectId": 1,
29+
"propertyId": "presentValue"
30+
},
31+
{
32+
"key": "presure",
33+
"objectType": "analogInput",
34+
"objectId": 2,
35+
"propertyId": "presentValue"
36+
}
37+
],
38+
"attributes": [
39+
{
40+
"key": "binaryInput1",
41+
"objectType": "binaryInput",
42+
"objectId": 1,
43+
"propertyId": "presentValue"
44+
},
45+
{
46+
"key": "binaryValue2",
47+
"objectType": "binaryValue",
48+
"objectId": 2,
49+
"propertyId": "presentValue"
50+
},
51+
{
52+
"key": "binaryOutput",
53+
"objectType": "binaryOutput",
54+
"objectId": 1,
55+
"propertyId": "presentValue"
56+
}
57+
],
58+
"attributeUpdates": [
59+
{
60+
"key": "binaryValue2",
61+
"objectType": "binaryValue",
62+
"objectId": 2,
63+
"propertyId": "presentValue"
64+
},
65+
{
66+
"key": "binaryInput1",
67+
"objectType": "binaryInput",
68+
"objectId": 33323232442342342,
69+
"propertyId": "presentValue"
70+
}
71+
],
72+
"serverSideRpc": [],
73+
"address": "192.168.1.136:47809",
74+
"devicesRescanObjectsPeriodSeconds": 60
75+
76+
77+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"altResponsesAddresses": [],
3+
"deviceInfo": {
4+
"deviceNameExpression": "test emulator device",
5+
"deviceNameExpressionSource": "expression",
6+
"deviceProfileExpressionSource": "constant",
7+
"deviceProfileExpression": "default"
8+
},
9+
"pollPeriod": 10000,
10+
"timeseries": [
11+
{
12+
"key": "power",
13+
"objectType": "analogValue",
14+
"objectId": 1,
15+
"propertyId": "presentValue"
16+
},
17+
{
18+
"key": "frequency",
19+
"objectType": "analogValue",
20+
"objectId": 2,
21+
"propertyId": "presentValue"
22+
},
23+
{
24+
"key": "temperature",
25+
"objectType": "analogOutput",
26+
"objectId": 1,
27+
"propertyId": "presentValue"
28+
},
29+
{
30+
"key": "presure",
31+
"objectType": "analogInput",
32+
"objectId": 2,
33+
"propertyId": "presentValue"
34+
}
35+
],
36+
"attributes": [
37+
{
38+
"key": "binaryInput1",
39+
"objectType": "binaryInput",
40+
"objectId": 1,
41+
"propertyId": "presentValue"
42+
},
43+
{
44+
"key": "binaryValue2",
45+
"objectType": "binaryValue",
46+
"objectId": 2,
47+
"propertyId": "presentValue"
48+
},
49+
{
50+
"key": "binaryOutput",
51+
"objectType": "binaryOutput",
52+
"objectId": 1,
53+
"propertyId": "presentValue"
54+
}
55+
],
56+
"attributeUpdates": [
57+
{
58+
"key": "binaryValue2",
59+
"objectType": "binaryValue",
60+
"objectId": true,
61+
"propertyId": "presentValue"
62+
}
63+
],
64+
"serverSideRpc": [],
65+
"address": "192.168.1.136:47809",
66+
"devicesRescanObjectsPeriodSeconds": 60
67+
}

0 commit comments

Comments
 (0)