test_bacnet_service.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. import asyncio
  2. import os
  3. import unittest
  4. from unittest.mock import patch
  5. from app.schemas.bacnet import BacnetPointReadRequest, BacnetPointSearchRequest
  6. from app.services import bacnet_service
  7. class FailingBacnetApp:
  8. async def read_property(self, *_args):
  9. raise RuntimeError("timeout")
  10. class FailingBacnetContext:
  11. def __init__(self):
  12. self.this_application = type("ThisApplication", (), {"app": FailingBacnetApp()})()
  13. async def __aenter__(self):
  14. return self
  15. async def __aexit__(self, exc_type, exc, tb):
  16. return False
  17. class SearchBacnetApp:
  18. async def read_property(self, _address, objid, prop, array_index=None):
  19. if str(objid) == "device,12345" and str(prop) == "object-list" and array_index == 0:
  20. return 1
  21. if str(objid) == "device,12345" and str(prop) == "object-list" and array_index == 1:
  22. return ("analog-input", 1)
  23. if str(objid) == "analog-input,1" and str(prop) == "object-name":
  24. return "ai-1"
  25. if str(objid) == "analog-input,1" and str(prop) == "description":
  26. return "temperature"
  27. if str(objid) == "analog-input,1" and str(prop) == "present-value":
  28. return 12.3
  29. raise RuntimeError("unexpected read")
  30. class SearchBacnetContext:
  31. def __init__(self):
  32. self.this_application = type("ThisApplication", (), {"app": SearchBacnetApp()})()
  33. async def __aenter__(self):
  34. return self
  35. async def __aexit__(self, exc_type, exc, tb):
  36. return False
  37. class BacnetServiceTest(unittest.TestCase):
  38. def test_read_points_returns_clear_no_response_error(self):
  39. original_start = bacnet_service.BAC0.start
  40. original_local_ip = bacnet_service.local_bacnet_ip
  41. original_local_port = bacnet_service.get_free_udp_port
  42. bacnet_service.BAC0.start = lambda **_kwargs: FailingBacnetContext()
  43. bacnet_service.local_bacnet_ip = lambda _ip, _port: "127.0.0.1/24"
  44. bacnet_service.get_free_udp_port = lambda: 47809
  45. self.addCleanup(setattr, bacnet_service.BAC0, "start", original_start)
  46. self.addCleanup(setattr, bacnet_service, "local_bacnet_ip", original_local_ip)
  47. self.addCleanup(setattr, bacnet_service, "get_free_udp_port", original_local_port)
  48. response = bacnet_service.read_points(
  49. BacnetPointReadRequest(
  50. ip="192.168.1.10",
  51. bacnet_device_id=12345,
  52. points=[{"object_type": "analogInput", "object_id": 1}],
  53. )
  54. )
  55. self.assertEqual(1, response["code"])
  56. self.assertIn("no response from BACnet device 192.168.1.10:47808", response["msg"])
  57. self.assertEqual([], response["data"]["points"])
  58. def test_search_points_reads_object_list_by_index(self):
  59. original_start = bacnet_service.BAC0.start
  60. original_local_ip = bacnet_service.local_bacnet_ip
  61. original_local_port = bacnet_service.get_free_udp_port
  62. bacnet_service.BAC0.start = lambda **_kwargs: SearchBacnetContext()
  63. bacnet_service.local_bacnet_ip = lambda _ip, _port: "127.0.0.1/24"
  64. bacnet_service.get_free_udp_port = lambda: 47809
  65. self.addCleanup(setattr, bacnet_service.BAC0, "start", original_start)
  66. self.addCleanup(setattr, bacnet_service, "local_bacnet_ip", original_local_ip)
  67. self.addCleanup(setattr, bacnet_service, "get_free_udp_port", original_local_port)
  68. response = bacnet_service.search_points(
  69. BacnetPointSearchRequest(ip="192.168.1.10", bacnet_device_id=12345)
  70. )
  71. self.assertEqual(0, response["code"])
  72. self.assertEqual(
  73. [
  74. {
  75. "name": "ai-1",
  76. "description": "temperature",
  77. "object_type": "AnalogInput",
  78. "object_id": 1,
  79. "present_value": 12.3,
  80. }
  81. ],
  82. response["data"]["points"],
  83. )
  84. def test_ignores_cancelled_broadcast_endpoint_callback(self):
  85. class Loop:
  86. default_handler_called = False
  87. def default_exception_handler(self, _context):
  88. self.default_handler_called = True
  89. loop = Loop()
  90. bacnet_service.ignore_cancelled_bacnet_broadcast_endpoint(
  91. loop,
  92. {
  93. "message": "Exception in callback IPv4DatagramServer.set_broadcast_transport_protocol()",
  94. "exception": asyncio.CancelledError(),
  95. },
  96. )
  97. self.assertFalse(loop.default_handler_called)
  98. def test_builds_bbmd_whois_as_distributed_local_broadcast(self):
  99. packet = bacnet_service.bbmd_whois_packet(0, 4_194_303)
  100. self.assertEqual(0x81, packet[0])
  101. self.assertEqual(0x09, packet[1])
  102. self.assertEqual(len(packet), int.from_bytes(packet[2:4], "big"))
  103. self.assertEqual(b"\x01\x00\x10\x08", packet[4:8])
  104. def test_parses_forwarded_i_am_packet(self):
  105. original_sender = b"\xc0\xa8\x01\x0a\xba\xc0"
  106. object_id = ((8 << 22) | 12345).to_bytes(4, "big")
  107. apdu = b"\x10\x00" + b"\xc4" + object_id + b"\x22\x05\xc4" + b"\x91\x03" + b"\x22\x03\x4a"
  108. npdu = b"\x01\x00" + apdu
  109. payload = original_sender + npdu
  110. packet = b"\x81\x04" + (4 + len(payload)).to_bytes(2, "big") + payload
  111. device = bacnet_service.parse_i_am_packet(packet, ("192.168.1.1", 47808))
  112. self.assertEqual(
  113. {
  114. "bacnet_device_id": 12345,
  115. "max_apdu": 1476,
  116. "segmentation": "noSegmentation",
  117. "vendor_id": 842,
  118. "ip": "192.168.1.10",
  119. "port": 47808,
  120. },
  121. device,
  122. )
  123. def test_bbmd_whois_requires_configured_bbmd_ip(self):
  124. with patch.dict(os.environ, {}, clear=True):
  125. response = bacnet_service.bbmd_whois()
  126. self.assertEqual(1, response["code"])
  127. self.assertIn("BACNET_BBMD_IP environment variable is required", response["msg"])
  128. self.assertEqual([], response["data"]["devices"])
  129. def test_builds_bbmd_whois_request_from_environment(self):
  130. with patch.dict(
  131. os.environ,
  132. {
  133. "BACNET_BBMD_IP": "192.168.1.1",
  134. "BACNET_BBMD_PORT": "47809",
  135. "BACNET_BBMD_TTL": "120",
  136. "BACNET_BBMD_WHOIS_TIMEOUT": "7.5",
  137. },
  138. clear=True,
  139. ):
  140. request = bacnet_service.bbmd_whois_request_from_env()
  141. self.assertEqual("192.168.1.1", request.bbmd_ip)
  142. self.assertEqual(47809, request.bbmd_port)
  143. self.assertEqual(120, request.ttl)
  144. self.assertEqual(7.5, request.timeout)
  145. if __name__ == "__main__":
  146. unittest.main()