Преглед на файлове

修改bacnet读取点位

Lu Xianghui преди 1 месец
родител
ревизия
c3be1f9b90
променени са 2 файла, в които са добавени 103 реда и са изтрити 10 реда
  1. 40 7
      app/services/bacnet_service.py
  2. 63 3
      intergration/bacnet/test_bacnet_service.py

+ 40 - 7
app/services/bacnet_service.py

@@ -13,6 +13,10 @@ BAC0_REGISTERED_WARNING = r"object type .* for vendor identifier 842 already reg
 warnings.filterwarnings("ignore", message=BAC0_REGISTERED_WARNING)
 
 import BAC0
+from bacpypes3.apdu import ErrorRejectAbortNack
+from bacpypes3.basetypes import PropertyIdentifier
+from bacpypes3.pdu import Address
+from bacpypes3.primitivedata import ObjectIdentifier
 
 from app.response import response_payload
 from app.schemas.bacnet import (
@@ -63,8 +67,23 @@ def bacnet_address(ip: str, port: int) -> str:
     return f"{ip}:{port}"
 
 
-def property_read_args(address: str, object_type: str, object_id: int, property_name: str) -> str:
-    return f"{address} {bac0_object_type(object_type)} {object_id} {property_name}"
+async def read_property_direct(
+    bacnet: Any,
+    address: str,
+    object_type: str,
+    object_id: int,
+    property_name: str,
+    array_index: int | None = None,
+) -> Any:
+    value = await bacnet.this_application.app.read_property(
+        Address(address),
+        ObjectIdentifier((bac0_object_type(object_type), object_id)),
+        PropertyIdentifier(property_name),
+        array_index,
+    )
+    if isinstance(value, ErrorRejectAbortNack):
+        raise BacnetCommunicationError(str(value))
+    return value
 
 
 def get_local_ip(remote_ip: str, remote_port: int) -> str:
@@ -105,7 +124,7 @@ def json_value(value: Any) -> Any:
 
 async def read_property_or_none(bacnet: Any, address: str, object_type: str, object_id: int, property_name: str) -> Any:
     try:
-        value = await bacnet.read(property_read_args(address, object_type, object_id, property_name))
+        value = await read_property_direct(bacnet, address, object_type, object_id, property_name)
     except Exception:
         return None
     return json_value(value)
@@ -113,7 +132,7 @@ async def read_property_or_none(bacnet: Any, address: str, object_type: str, obj
 
 async def read_present_value(bacnet: Any, address: str, point: BacnetPointSpec) -> Any:
     try:
-        value = await bacnet.read(property_read_args(address, point.object_type, point.object_id, "presentValue"))
+        value = await read_property_direct(bacnet, address, point.object_type, point.object_id, "presentValue")
     except Exception as exc:
         raise BacnetCommunicationError(
             f"no response from BACnet device {address} while reading "
@@ -200,15 +219,29 @@ async def search_points_async(request: BacnetPointSearchRequest) -> list[dict[st
         warnings.filterwarnings("ignore", message=BAC0_REGISTERED_WARNING)
         async with BAC0.start(ip=local_ip, port=local_port, ping=False) as bacnet:
             try:
-                object_list = await bacnet.read(
-                    property_read_args(address, "Device", request.bacnet_device_id, "objectList")
+                object_count = await read_property_direct(
+                    bacnet,
+                    address,
+                    "Device",
+                    request.bacnet_device_id,
+                    "objectList",
+                    array_index=0,
                 )
             except Exception as exc:
                 raise BacnetCommunicationError(
                     f"no response from BACnet device {address} while reading "
                     f"Device {request.bacnet_device_id} objectList"
                 ) from exc
-            for item in object_list or []:
+
+            for index in range(1, int(object_count) + 1):
+                item = await read_property_direct(
+                    bacnet,
+                    address,
+                    "Device",
+                    request.bacnet_device_id,
+                    "objectList",
+                    array_index=index,
+                )
                 parsed = split_object_identifier(item)
                 if parsed is None:
                     continue

+ 63 - 3
intergration/bacnet/test_bacnet_service.py

@@ -1,19 +1,50 @@
 import asyncio
 import unittest
 
-from app.schemas.bacnet import BacnetPointReadRequest
+from app.schemas.bacnet import BacnetPointReadRequest, BacnetPointSearchRequest
 from app.services import bacnet_service
 
 
+class FailingBacnetApp:
+    async def read_property(self, *_args):
+        raise RuntimeError("timeout")
+
+
 class FailingBacnetContext:
+    def __init__(self):
+        self.this_application = type("ThisApplication", (), {"app": FailingBacnetApp()})()
+
     async def __aenter__(self):
         return self
 
     async def __aexit__(self, exc_type, exc, tb):
         return False
 
-    async def read(self, _args):
-        raise RuntimeError("timeout")
+
+class SearchBacnetApp:
+    async def read_property(self, _address, objid, prop, array_index=None):
+        if str(objid) == "device,12345" and str(prop) == "object-list" and array_index == 0:
+            return 1
+        if str(objid) == "device,12345" and str(prop) == "object-list" and array_index == 1:
+            return ("analog-input", 1)
+        if str(objid) == "analog-input,1" and str(prop) == "object-name":
+            return "ai-1"
+        if str(objid) == "analog-input,1" and str(prop) == "description":
+            return "temperature"
+        if str(objid) == "analog-input,1" and str(prop) == "present-value":
+            return 12.3
+        raise RuntimeError("unexpected read")
+
+
+class SearchBacnetContext:
+    def __init__(self):
+        self.this_application = type("ThisApplication", (), {"app": SearchBacnetApp()})()
+
+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc, tb):
+        return False
 
 
 class BacnetServiceTest(unittest.TestCase):
@@ -40,6 +71,35 @@ class BacnetServiceTest(unittest.TestCase):
         self.assertIn("no response from BACnet device 192.168.1.10:47808", response["msg"])
         self.assertEqual([], response["data"]["points"])
 
+    def test_search_points_reads_object_list_by_index(self):
+        original_start = bacnet_service.BAC0.start
+        original_local_ip = bacnet_service.local_bacnet_ip
+        original_local_port = bacnet_service.get_free_udp_port
+        bacnet_service.BAC0.start = lambda **_kwargs: SearchBacnetContext()
+        bacnet_service.local_bacnet_ip = lambda _ip, _port: "127.0.0.1/24"
+        bacnet_service.get_free_udp_port = lambda: 47809
+        self.addCleanup(setattr, bacnet_service.BAC0, "start", original_start)
+        self.addCleanup(setattr, bacnet_service, "local_bacnet_ip", original_local_ip)
+        self.addCleanup(setattr, bacnet_service, "get_free_udp_port", original_local_port)
+
+        response = bacnet_service.search_points(
+            BacnetPointSearchRequest(ip="192.168.1.10", bacnet_device_id=12345)
+        )
+
+        self.assertEqual(0, response["code"])
+        self.assertEqual(
+            [
+                {
+                    "name": "ai-1",
+                    "description": "temperature",
+                    "object_type": "AnalogInput",
+                    "object_id": 1,
+                    "present_value": 12.3,
+                }
+            ],
+            response["data"]["points"],
+        )
+
     def test_ignores_cancelled_broadcast_endpoint_callback(self):
         class Loop:
             default_handler_called = False