55from functools import cached_property
66from typing import Any , Dict , List , Optional , Tuple
77
8+ from buildingmotif .ingresses .base import Record , RecordIngressHandler
9+
810try :
911 import BAC0
1012except ImportError :
1315 )
1416
1517
16- from buildingmotif .ingresses .base import Record , RecordIngressHandler
17-
18- # We do this little rigamarole to avoid BAC0 spitting out a million
19- # logging messages warning us that we changed the log level, which
20- # happens when we go through the normal BAC0 log level procedure
21- logger = logging .getLogger ("BAC0_Root.BAC0.scripts.Base.Base" )
22- logger .setLevel (logging .ERROR )
23-
24-
2518class BACnetNetwork (RecordIngressHandler ):
2619 def __init__ (
2720 self ,
@@ -87,7 +80,6 @@ async def _collect_objects(
8780 device_kwargs .setdefault ("poll" , - 1 )
8881 device_kwargs .setdefault ("auto_save" , False )
8982
90- logger .error (f"starting with { ip = } { ping = } " )
9183 async with BAC0 .start (ip = ip , ping = ping ) as bacnet :
9284 await asyncio .sleep (2 )
9385 await bacnet ._discover (** discover_kwargs )
@@ -110,7 +102,7 @@ async def _collect_objects(
110102 device_id = info .get ("device_id" )
111103
112104 if address is None or device_id is None :
113- logger .warning (
105+ logging .warning (
114106 "Skipping discovered device with missing address/device_id: %s" ,
115107 info ,
116108 )
@@ -128,6 +120,13 @@ async def _collect_objects(
128120 for (address , device_id , _ ) in discovered_entries :
129121 device = await BAC0 .device (address , device_id , bacnet , ** device_kwargs )
130122 try :
123+ # keep persistence disabled and quiet for one-shot scans
124+ setattr (device .properties , "auto_save" , False )
125+ setattr (device .properties , "clear_history_on_save" , False )
126+ setattr (device .properties , "history_size" , None )
127+ if hasattr (device , "_log" ):
128+ device ._log .setLevel (logging .ERROR ) # type: ignore[attr-defined]
129+
131130 objects : List [Dict [str , Any ]] = []
132131
133132 for bobj in device .points :
@@ -137,14 +136,36 @@ async def _collect_objects(
137136
138137 self .objects [(address , device_id )] = objects
139138 finally :
140- disconnect_task = device .disconnect (save_on_disconnect = False )
141- if disconnect_task is not None :
142- await disconnect_task
139+ disconnect = getattr (
140+ device , "_disconnect" , None # type: ignore[attr-defined]
141+ )
142+ if callable (disconnect ):
143+ await disconnect (save_on_disconnect = False , unregister = True )
143144
144145 def _clean_object (self , obj : Dict [str , Any ]):
145- if "name" in obj :
146+ def _normalize (value : Any , path : Tuple [Any , ...]) -> Any :
147+ if isinstance (value , (str , int , float , bool )) or value is None :
148+ return value
149+ if isinstance (value , dict ):
150+ normalized : Dict [Any , Any ] = {}
151+ for key , nested in value .items ():
152+ normalized [key ] = _normalize (nested , (* path , key ))
153+ return normalized
154+ if isinstance (value , (list , tuple , set )):
155+ return [_normalize (v , (* path , idx )) for idx , v in enumerate (value )]
156+
157+ logging .error (
158+ "Ignoring non-serializable BACnet value %r at %s" ,
159+ value ,
160+ " -> " .join (str (p ) for p in path ),
161+ )
162+ return None
163+
164+ if "name" in obj and isinstance (obj ["name" ], str ):
146165 # remove trailing/leading whitespace from names
147166 obj ["name" ] = obj ["name" ].strip ()
167+ for key , value in list (obj .items ()):
168+ obj [key ] = _normalize (value , (obj .get ("device" ), key ))
148169
149170 @cached_property
150171 def records (self ) -> List [Record ]:
0 commit comments