Skip to content
Merged
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
06ae259
new tests added
timyr220 Jan 27, 2025
8708e58
changed the data
timyr220 Jan 27, 2025
fc2c0b2
updated unit test
timyr220 Jan 30, 2025
80e9aee
added rate limit tests
timyr220 Feb 6, 2025
7b78140
updated tests
timyr220 Feb 6, 2025
49b7e4d
firmware
timyr220 Feb 6, 2025
1758a31
added firmware tests
timyr220 Feb 6, 2025
55478e6
licence added
timyr220 Feb 6, 2025
2772434
licence added
timyr220 Feb 6, 2025
44a1ded
new tests have been added
timyr220 Feb 6, 2025
c783d0f
Fixed Thread by replacing target=self.__service_loop_func and then d…
timyr220 Feb 6, 2025
393bbcc
Fixed Thread by replacing target=self.__service_loop_func and then d…
timyr220 Feb 6, 2025
bda2d00
new tests added
timyr220 Feb 11, 2025
e233dcf
new tests added
timyr220 Feb 11, 2025
2a63323
new tests added
timyr220 Feb 11, 2025
5fe2c1c
minor modifications
timyr220 Feb 20, 2025
7d3046f
added firmware tests
timyr220 Feb 20, 2025
8bac088
added rate limit tests
timyr220 Feb 20, 2025
c6ba78b
added tests of tb_device_mqtt operation
timyr220 Feb 20, 2025
c432320
split message tests added
timyr220 Feb 20, 2025
ccd385f
added rpc reply tests
timyr220 Feb 20, 2025
abed2c8
added decoded message tests
timyr220 Feb 20, 2025
12d1781
added tb_gateway_mqtt tests
timyr220 Feb 20, 2025
bb9e2b3
added init_gateway tests
timyr220 Feb 20, 2025
f240f1b
added tests
timyr220 Feb 20, 2025
49c39d7
licence added and comments removed
timyr220 Feb 20, 2025
f63bc20
bug fixes
timyr220 Feb 21, 2025
1d0eb8d
add new tests
timyr220 Feb 21, 2025
baf987f
add new tests
timyr220 Feb 21, 2025
909de92
add new tests
timyr220 Feb 21, 2025
99e71a6
modified tests
timyr220 Mar 3, 2025
9a800ce
modified tests
timyr220 Mar 3, 2025
7f9e50f
modified tests
timyr220 Mar 3, 2025
46914ca
modified tests
timyr220 Mar 3, 2025
d212e36
modified tests
timyr220 Mar 3, 2025
85c4af8
modified tests
timyr220 Mar 3, 2025
dd542b3
modified tests
timyr220 Mar 3, 2025
2593264
Changed data
timyr220 Mar 3, 2025
d1af450
Removed blank lines
timyr220 Mar 5, 2025
10c55b5
Updated test
timyr220 Mar 5, 2025
04debc8
Updated logic
timyr220 Mar 5, 2025
38a5dcb
Updated logic
timyr220 Mar 5, 2025
932ff2a
Data changed and logic changed
timyr220 Mar 6, 2025
e9f2147
Updated data
timyr220 Mar 6, 2025
8820e75
New test added
timyr220 Mar 6, 2025
607f5cd
New test added
timyr220 Mar 6, 2025
ad084f2
changes removed
timyr220 Mar 7, 2025
c72e13d
Add TBDeviceMqttClient tests
timyr220 Mar 11, 2025
069c13b
Fix TBDeviceMqttClient tests
timyr220 Mar 11, 2025
5ec7ca6
Fix TBDeviceMqttClient tests
timyr220 Mar 11, 2025
94a8492
Fix TBDeviceMqttClient tests
timyr220 Mar 11, 2025
f79bbe6
Refactor TBDeviceMqttClient tests
timyr220 Mar 11, 2025
576aede
Refactor TBDeviceMqttClient tests
timyr220 Mar 11, 2025
9e7c480
Fix TBDeviceMqttClient tests
timyr220 Mar 11, 2025
c1a6603
Test fix
timyr220 Mar 12, 2025
0f2e2db
Function removed
timyr220 Mar 14, 2025
c4216ff
changed function name
timyr220 Mar 14, 2025
02b501b
New test added
timyr220 Mar 14, 2025
f0ae78b
fixed tests and replaced logic
timyr220 Mar 24, 2025
84462e7
changed data
timyr220 Mar 24, 2025
f6288b6
class removed
timyr220 Mar 24, 2025
962b57c
removed unnecessary classes and strings
timyr220 Mar 24, 2025
6ccbd88
back to the old version
timyr220 Mar 26, 2025
27e04fc
bug fixes
timyr220 Mar 26, 2025
1069459
resolve merge conflicts from GitHub
timyr220 Mar 26, 2025
0da9ba6
fix: force conflict resolution for tb_device_mqtt.py
timyr220 Mar 26, 2025
cdc74b1
Merge branch 'master' into master
timyr220 Mar 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions tb_device_mqtt.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Copyright 2024. 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.
Expand Down Expand Up @@ -125,20 +125,21 @@ class TBPublishInfo:
def __init__(self, message_info):
self.message_info = message_info


# pylint: disable=invalid-name
def rc(self):
if isinstance(self.message_info, list):
for info in self.message_info:
if isinstance(info.rc, ReasonCodes):
if isinstance(info.rc, ReasonCodes) or hasattr(info.rc, 'value'):
if info.rc.value == 0:
continue
return info.rc
return info.rc.value
else:
if info.rc != 0:
return info.rc
return self.TB_ERR_SUCCESS
else:
if isinstance(self.message_info.rc, ReasonCodes):
if isinstance(self.message_info.rc, ReasonCodes) or hasattr(self.message_info.rc, 'value'):
return self.message_info.rc.value
return self.message_info.rc

Expand Down Expand Up @@ -239,6 +240,11 @@ def set_limit(self, rate_limit, percentage=80):
old_rate_limit_dict = deepcopy(self._rate_limit_dict)
self._rate_limit_dict = {}
self.percentage = percentage if percentage != 0 else self.percentage
if rate_limit.strip() == "0:0,":
self._rate_limit_dict.clear()
self._no_limit = True
log.debug("Rate limit set to NO_LIMIT from '0:0,' directive.")
return
rate_configs = rate_limit.split(";")
if "," in rate_limit:
rate_configs = rate_limit.split(",")
Expand Down Expand Up @@ -673,6 +679,7 @@ def send_rpc_reply(self, req_id, resp, quality_of_service=None, wait_for_publish
info = self._publish_data(resp, RPC_RESPONSE_TOPIC + req_id, quality_of_service)
if wait_for_publish:
info.get()
return info

def send_rpc_call(self, method, params, callback):
"""Send RPC call to ThingsBoard. The callback will be called when the response is received."""
Expand Down
78 changes: 78 additions & 0 deletions tests/count_data_points_message_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2025. 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 unittest
from tb_device_mqtt import TBDeviceMqttClient


class TestCountDataPointsInMessage(unittest.TestCase):
def test_simple_dict_no_device(self):
data = {
"ts": 123456789,
"values": {
"temp": 22.5,
"humidity": 55
}
}
result = TBDeviceMqttClient._count_datapoints_in_message(data)
self.assertEqual(result, 2)

def test_list_of_dict_no_device(self):
data = [
{"ts": 123456789, "values": {"temp": 22.5, "humidity": 55}},
{"ts": 123456799, "values": {"light": 100, "pressure": 760}}
]
result = TBDeviceMqttClient._count_datapoints_in_message(data)
self.assertEqual(result, 4)

def test_with_device_dict_inside(self):
data = {
"MyDevice": {
"ts": 123456789,
"values": {"temp": 22.5, "humidity": 55}
},
"OtherKey": "some_value"
}
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="MyDevice")
self.assertEqual(result, 2)

def test_with_device_list_inside(self):
data = {
"Sensor": [
{"ts": 1, "values": {"v1": 10}},
{"ts": 2, "values": {"v2": 20, "v3": 30}}
]
}
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="Sensor")
self.assertEqual(result, 3)

def test_empty_dict_no_device(self):
data = {}
result = TBDeviceMqttClient._count_datapoints_in_message(data)
self.assertEqual(result, 0)

def test_missing_device_key(self):

data = {"some_unrelated_key": 42}
result = TBDeviceMqttClient._count_datapoints_in_message(data, device="NotExistingDeviceKey")
self.assertEqual(result, 1)

def test_data_is_string_no_device(self):
data = "just a string"
result = TBDeviceMqttClient._count_datapoints_in_message(data)
self.assertEqual(result, 1)


if __name__ == '__main__':
unittest.main()
220 changes: 220 additions & 0 deletions tests/firmware_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
# Copyright 2025. 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 unittest
from unittest.mock import patch, MagicMock, call
from math import ceil
import orjson
from threading import Thread
from tb_device_mqtt import (
TBDeviceMqttClient,
TBTimeoutException,
FW_VERSION_ATTR, FW_TITLE_ATTR, FW_STATE_ATTR
)
from paho.mqtt.client import ReasonCodes


FW_TITLE_ATTR = "fw_title"
FW_VERSION_ATTR = "fw_version"
REQUIRED_SHARED_KEYS = "dummy_shared_keys"


class TestFirmwareUpdateBranch(unittest.TestCase):
@patch('tb_device_mqtt.sleep', return_value=None, autospec=True)
@patch('tb_device_mqtt.log.debug', autospec=True)
def test_firmware_update_branch(self, _, mock_sleep):
client = TBDeviceMqttClient('fake_host', username="dummy_token", password="dummy")
client._TBDeviceMqttClient__service_loop = lambda: None
client._TBDeviceMqttClient__timeout_check = lambda: None

client._messages_rate_limit = MagicMock()

client.current_firmware_info = {
"current_" + FW_VERSION_ATTR: "v0",
FW_STATE_ATTR: "IDLE"
}
client.firmware_data = b"old_data"
client._TBDeviceMqttClient__current_chunk = 2
client._TBDeviceMqttClient__firmware_request_id = 0
client._TBDeviceMqttClient__chunk_size = 128
client._TBDeviceMqttClient__target_firmware_length = 0

client.send_telemetry = MagicMock()
client._TBDeviceMqttClient__get_firmware = MagicMock()

message_mock = MagicMock()
message_mock.topic = "v1/devices/me/attributes_update"
payload_dict = {
"fw_version": "v1",
"fw_title": "TestFirmware",
"fw_size": 900
}
message_mock.payload = orjson.dumps(payload_dict)

client._on_decoded_message({}, message_mock)
client.stopped = True

client._messages_rate_limit.increase_rate_limit_counter.assert_called_once()

self.assertEqual(client.firmware_data, b"")
self.assertEqual(client._TBDeviceMqttClient__current_chunk, 0)
self.assertEqual(client.current_firmware_info[FW_STATE_ATTR], "DOWNLOADING")

client.send_telemetry.assert_called_once_with(client.current_firmware_info)

sleep_called = any(args and (args[0] == 1 or args[0] == 1.0) for args, kwargs in mock_sleep.call_args_list)
self.assertTrue(sleep_called, f"sleep(2) was not called, calls: {mock_sleep.call_args_list}")

self.assertEqual(client._TBDeviceMqttClient__firmware_request_id, 1)
self.assertEqual(client._TBDeviceMqttClient__target_firmware_length, 900)
self.assertEqual(client._TBDeviceMqttClient__chunk_count, ceil(900 / 128))
client._TBDeviceMqttClient__get_firmware.assert_called_once()



class TestTBDeviceMqttClient(unittest.TestCase):
@patch('tb_device_mqtt.paho.Client')
def setUp(self, mock_paho_client):
self.mock_mqtt_client = mock_paho_client.return_value
self.client = TBDeviceMqttClient(
host='host',
port=1883,
username='username',
password=None
)
self.client.firmware_info = {FW_TITLE_ATTR: "dummy_firmware.bin"}
self.client.firmware_data = b''
self.client._TBDeviceMqttClient__current_chunk = 0
self.client._TBDeviceMqttClient__firmware_request_id = 1
self.client._TBDeviceMqttClient__service_loop = Thread(target=lambda: None)
self.client._TBDeviceMqttClient__updating_thread = Thread(target=lambda: None)
self.client._publish_data = MagicMock()

if not hasattr(self.client, '_client'):
self.client._client = self.mock_mqtt_client

def test_get_firmware_update(self):
self.client._client.subscribe = MagicMock()
self.client.send_telemetry = MagicMock()
self.client.get_firmware_update()
self.client._client.subscribe.assert_called_with('v2/fw/response/+')
self.client.send_telemetry.assert_called()
self.client._publish_data.assert_called()

def test_firmware_download_process(self):
self.client.firmware_info = {
FW_TITLE_ATTR: "dummy_firmware.bin",
FW_VERSION_ATTR: "2.0",
"fw_size": 1024,
"fw_checksum": "abc123",
"fw_checksum_algorithm": "SHA256"
}
self.client._TBDeviceMqttClient__current_chunk = 0
self.client._TBDeviceMqttClient__firmware_request_id = 1
self.client._TBDeviceMqttClient__get_firmware()
self.client._publish_data.assert_called()

def test_firmware_verification_success(self):
self.client.firmware_data = b'binary data'
self.client.firmware_info = {
FW_TITLE_ATTR: "dummy_firmware.bin",
FW_VERSION_ATTR: "2.0",
"fw_checksum": "valid_checksum",
"fw_checksum_algorithm": "SHA256"
}
self.client._TBDeviceMqttClient__process_firmware()
self.client._publish_data.assert_called()

def test_firmware_verification_failure(self):
self.client.firmware_data = b'corrupt data'
self.client.firmware_info = {
FW_TITLE_ATTR: "dummy_firmware.bin",
FW_VERSION_ATTR: "2.0",
"fw_checksum": "invalid_checksum",
"fw_checksum_algorithm": "SHA256"
}
self.client._TBDeviceMqttClient__process_firmware()
self.client._publish_data.assert_called()

def test_firmware_state_transition(self):
self.client._publish_data.reset_mock()
self.client.current_firmware_info = {
"current_fw_title": "OldFirmware",
"current_fw_version": "1.0",
"fw_state": "IDLE"
}
self.client.firmware_received = True
self.client.firmware_info[FW_TITLE_ATTR] = "dummy_firmware.bin"
self.client.firmware_info[FW_VERSION_ATTR] = "dummy_version"

with patch("builtins.open", new_callable=MagicMock) as m_open:
if hasattr(self.client, '_TBDeviceMqttClient__on_firmware_received'):
self.client._TBDeviceMqttClient__on_firmware_received("dummy_version")
m_open.assert_called_with("dummy_firmware.bin", "wb")

def test_firmware_request_info(self):
self.client._publish_data.reset_mock()
self.client._TBDeviceMqttClient__request_firmware_info()
self.client._publish_data.assert_called()

def test_firmware_chunk_reception(self):
self.client._publish_data.reset_mock()
self.client._TBDeviceMqttClient__get_firmware()
self.client._publish_data.assert_called()


class TestFirmwareUpdate(unittest.TestCase):
def setUp(self):
self.client = TBDeviceMqttClient(host="localhost", port=1883)
self.client._TBDeviceMqttClient__process_firmware = MagicMock()
self.client._TBDeviceMqttClient__get_firmware = MagicMock()

self.client._TBDeviceMqttClient__firmware_request_id = 1
self.client._TBDeviceMqttClient__current_chunk = 0
self.client._TBDeviceMqttClient__target_firmware_length = 10

self.client.firmware_data = b''

def test_incomplete_firmware_chunk(self):
chunk_data = b'abcde'
message = MagicMock()
message.topic = "v2/fw/response/1/chunk/0"
message.payload = chunk_data

self.client._on_message(None, None, message)
self.assertEqual(self.client.firmware_data, b'abcde')
self.assertEqual(self.client._TBDeviceMqttClient__current_chunk, 1)
self.client._TBDeviceMqttClient__process_firmware.assert_not_called()
self.client._TBDeviceMqttClient__get_firmware.assert_called_once()

def test_complete_firmware_chunk(self):
self.client.firmware_data = b'abcde'
self.client._TBDeviceMqttClient__current_chunk = 1

chunk_data = b'12345'
message = MagicMock()
message.topic = "v2/fw/response/1/chunk/1"
message.payload = chunk_data

self.client._on_message(None, None, message)

self.assertEqual(self.client.firmware_data, b'abcde12345')
self.assertEqual(self.client._TBDeviceMqttClient__current_chunk, 2)

self.client._TBDeviceMqttClient__process_firmware.assert_called_once()
self.client._TBDeviceMqttClient__get_firmware.assert_not_called()


if __name__ == '__main__':
unittest.main()
Loading