diff --git a/check_install.sh b/check_install.sh new file mode 100755 index 00000000..4abcc038 --- /dev/null +++ b/check_install.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +for version in 2.7 3.4 3.5 3.6 3.7 3.8; +do +if [ -a "`which python$version`" ]; then +python$version << EOF + +import sys +python_version = "%d.%d.%d" % sys.version_info[:3] + +try: + import bacpypes + print("%s: %s @ %s" %( + python_version, + bacpypes.__version__, bacpypes.__file__, + )) +except ImportError: + print("%s: not installed" % ( + python_version, + )) +EOF +fi +done diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 4ff0dc04..f11670ee 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.18.0' +__version__ = '0.18.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 2b7cc91f..2802911a 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -11,7 +11,8 @@ from .primitivedata import Boolean, CharacterString, Enumerated, Integer, \ ObjectIdentifier, ObjectType, OctetString, Real, TagList, Unsigned, \ expand_enumerations -from .constructeddata import Any, Choice, Element, Sequence, SequenceOf +from .constructeddata import Any, Choice, Element, \ + Sequence, SequenceOf, SequenceOfAny from .basetypes import ChannelValue, DateTime, DeviceAddress, ErrorType, \ EventState, EventTransitionBits, EventType, LifeSafetyOperation, \ NotificationParameters, NotifyType, ObjectPropertyReference, \ @@ -963,7 +964,7 @@ class ReadRangeACK(ComplexAckSequence): , Element('propertyArrayIndex', Unsigned, 2, True) , Element('resultFlags', ResultFlags, 3) , Element('itemCount', Unsigned, 4) - , Element('itemData', SequenceOf(Any), 5) + , Element('itemData', SequenceOfAny, 5) , Element('firstSequenceNumber', Unsigned, 6, True) ] diff --git a/py25/bacpypes/bvll.py b/py25/bacpypes/bvll.py index 2346e7e0..ec4b4e2d 100755 --- a/py25/bacpypes/bvll.py +++ b/py25/bacpypes/bvll.py @@ -522,7 +522,7 @@ def bvlpdu_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" foreign_device_table = [] for fdte in self.bvlciFDT: - foreign_device_table.append(fdte.dict_contents(as_class=as_class)) + foreign_device_table.append(fdte.bvlpdu_contents(as_class=as_class)) return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( diff --git a/py25/bacpypes/local/object.py b/py25/bacpypes/local/object.py index a4d55c37..014f604b 100644 --- a/py25/bacpypes/local/object.py +++ b/py25/bacpypes/local/object.py @@ -685,7 +685,7 @@ def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct= if _debug: Commandable._debug(" - priority_value: %r", priority_value) # the null or the choice has to be set, the other clear - if value is (): + if value == (): if _debug: Commandable._debug(" - write a null") priority_value.null = value setattr(priority_value, _Commando._pv_choice, None) diff --git a/py25/bacpypes/local/schedule.py b/py25/bacpypes/local/schedule.py index e9541bc3..6efa349a 100644 --- a/py25/bacpypes/local/schedule.py +++ b/py25/bacpypes/local/schedule.py @@ -358,6 +358,8 @@ def __init__(self, sched_obj): # add a monitor for the present value sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + sched_obj._property_monitors['weeklySchedule'].append(self.schedule_changed) + sched_obj._property_monitors['exceptionSchedule'].append(self.schedule_changed) # call to interpret the schedule deferred(self.process_task) @@ -406,6 +408,24 @@ def present_value_changed(self, old_value, new_value): except Exception as err: if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + def schedule_changed(self, old_value, new_value): + """This function is called when the weeklySchedule or the exceptionSchedule + property of the local schedule object has changed, both internally by + this interpreter, or externally by some client using WriteProperty.""" + if _debug: + LocalScheduleInterpreter._debug( + "schedule_changed(%s) %s %s", self.sched_obj.objectName, + old_value, new_value, + ) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # real work done by process_task + self.process_task() + def process_task(self): if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index c344e901..1bd19e7b 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -2215,7 +2215,7 @@ class ScheduleObject(Object): properties = \ [ ReadableProperty('presentValue', AnyAtomic) , ReadableProperty('effectivePeriod', DateRange) - , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) + , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule, 7)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 00a9a037..19241a58 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -61,7 +61,7 @@ def do_ReadPropertyRequest(self, apdu): raise PropertyError(apdu.propertyIdentifier) # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): if apdu.propertyArrayIndex == 0: @@ -164,7 +164,7 @@ def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): raise ExecutionError(errorClass='property', errorCode='unknownProperty') # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (propertyArrayIndex is not None): if propertyArrayIndex == 0: diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 4ff0dc04..f11670ee 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.18.0' +__version__ = '0.18.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index d18411f0..b272f3f9 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -11,8 +11,8 @@ from .primitivedata import Boolean, CharacterString, Enumerated, Integer, \ ObjectIdentifier, ObjectType, OctetString, Real, TagList, Unsigned, \ expand_enumerations -from .constructeddata import Any, Choice, Element, Sequence, SequenceOf, \ - SequenceOfAny +from .constructeddata import Any, Choice, Element, \ + Sequence, SequenceOf, SequenceOfAny from .basetypes import ChannelValue, DateTime, DeviceAddress, ErrorType, \ EventState, EventTransitionBits, EventType, LifeSafetyOperation, \ NotificationParameters, NotifyType, ObjectPropertyReference, \ diff --git a/py27/bacpypes/bvll.py b/py27/bacpypes/bvll.py index 30573b7d..ba30e5c1 100755 --- a/py27/bacpypes/bvll.py +++ b/py27/bacpypes/bvll.py @@ -519,7 +519,7 @@ def bvlpdu_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" foreign_device_table = [] for fdte in self.bvlciFDT: - foreign_device_table.append(fdte.dict_contents(as_class=as_class)) + foreign_device_table.append(fdte.bvlpdu_contents(as_class=as_class)) return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( diff --git a/py27/bacpypes/local/object.py b/py27/bacpypes/local/object.py index b789ce28..7932042f 100644 --- a/py27/bacpypes/local/object.py +++ b/py27/bacpypes/local/object.py @@ -683,7 +683,7 @@ def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct= if _debug: Commandable._debug(" - priority_value: %r", priority_value) # the null or the choice has to be set, the other clear - if value is (): + if value == (): if _debug: Commandable._debug(" - write a null") priority_value.null = value setattr(priority_value, _Commando._pv_choice, None) diff --git a/py27/bacpypes/local/schedule.py b/py27/bacpypes/local/schedule.py index 4c8c97a1..22019b94 100644 --- a/py27/bacpypes/local/schedule.py +++ b/py27/bacpypes/local/schedule.py @@ -357,6 +357,8 @@ def __init__(self, sched_obj): # add a monitor for the present value sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + sched_obj._property_monitors['weeklySchedule'].append(self.schedule_changed) + sched_obj._property_monitors['exceptionSchedule'].append(self.schedule_changed) # call to interpret the schedule deferred(self.process_task) @@ -405,6 +407,24 @@ def present_value_changed(self, old_value, new_value): except Exception as err: if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + def schedule_changed(self, old_value, new_value): + """This function is called when the weeklySchedule or the exceptionSchedule + property of the local schedule object has changed, both internally by + this interpreter, or externally by some client using WriteProperty.""" + if _debug: + LocalScheduleInterpreter._debug( + "schedule_changed(%s) %s %s", self.sched_obj.objectName, + old_value, new_value, + ) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # real work done by process_task + self.process_task() + def process_task(self): if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 56361a7b..2b541393 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -2170,7 +2170,7 @@ class ScheduleObject(Object): properties = \ [ ReadableProperty('presentValue', AnyAtomic) , ReadableProperty('effectivePeriod', DateRange) - , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) + , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule, 7)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index c74e7df1..f9b3b1e9 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -62,7 +62,7 @@ def do_ReadPropertyRequest(self, apdu): raise PropertyError(apdu.propertyIdentifier) # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): if apdu.propertyArrayIndex == 0: @@ -164,7 +164,7 @@ def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): raise ExecutionError(errorClass='property', errorCode='unknownProperty') # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (propertyArrayIndex is not None): if propertyArrayIndex == 0: diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 9058178e..96e60d08 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.18.0' +__version__ = '0.18.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index c6542100..b272f3f9 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -11,7 +11,8 @@ from .primitivedata import Boolean, CharacterString, Enumerated, Integer, \ ObjectIdentifier, ObjectType, OctetString, Real, TagList, Unsigned, \ expand_enumerations -from .constructeddata import Any, Choice, Element, Sequence, SequenceOf +from .constructeddata import Any, Choice, Element, \ + Sequence, SequenceOf, SequenceOfAny from .basetypes import ChannelValue, DateTime, DeviceAddress, ErrorType, \ EventState, EventTransitionBits, EventType, LifeSafetyOperation, \ NotificationParameters, NotifyType, ObjectPropertyReference, \ @@ -956,7 +957,7 @@ class ReadRangeACK(ComplexAckSequence): , Element('propertyArrayIndex', Unsigned, 2, True) , Element('resultFlags', ResultFlags, 3) , Element('itemCount', Unsigned, 4) - , Element('itemData', SequenceOf(Any), 5) + , Element('itemData', SequenceOfAny, 5) , Element('firstSequenceNumber', Unsigned, 6, True) ] diff --git a/py34/bacpypes/bvll.py b/py34/bacpypes/bvll.py index 30573b7d..ba30e5c1 100755 --- a/py34/bacpypes/bvll.py +++ b/py34/bacpypes/bvll.py @@ -519,7 +519,7 @@ def bvlpdu_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" foreign_device_table = [] for fdte in self.bvlciFDT: - foreign_device_table.append(fdte.dict_contents(as_class=as_class)) + foreign_device_table.append(fdte.bvlpdu_contents(as_class=as_class)) return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( diff --git a/py34/bacpypes/local/object.py b/py34/bacpypes/local/object.py index 42cb78f4..9f08fbcb 100644 --- a/py34/bacpypes/local/object.py +++ b/py34/bacpypes/local/object.py @@ -684,7 +684,7 @@ def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct= if _debug: Commandable._debug(" - priority_value: %r", priority_value) # the null or the choice has to be set, the other clear - if value is (): + if value == (): if _debug: Commandable._debug(" - write a null") priority_value.null = value setattr(priority_value, _Commando._pv_choice, None) diff --git a/py34/bacpypes/local/schedule.py b/py34/bacpypes/local/schedule.py index 91d6ccf7..0f9cf7e5 100644 --- a/py34/bacpypes/local/schedule.py +++ b/py34/bacpypes/local/schedule.py @@ -357,6 +357,8 @@ def __init__(self, sched_obj): # add a monitor for the present value sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + sched_obj._property_monitors['weeklySchedule'].append(self.schedule_changed) + sched_obj._property_monitors['exceptionSchedule'].append(self.schedule_changed) # call to interpret the schedule deferred(self.process_task) @@ -405,6 +407,24 @@ def present_value_changed(self, old_value, new_value): except Exception as err: if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + def schedule_changed(self, old_value, new_value): + """This function is called when the weeklySchedule or the exceptionSchedule + property of the local schedule object has changed, both internally by + this interpreter, or externally by some client using WriteProperty.""" + if _debug: + LocalScheduleInterpreter._debug( + "schedule_changed(%s) %s %s", self.sched_obj.objectName, + old_value, new_value, + ) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # real work done by process_task + self.process_task() + def process_task(self): if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index fdff04bd..23fc7b5e 100644 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -2169,7 +2169,7 @@ class ScheduleObject(Object): properties = \ [ ReadableProperty('presentValue', AnyAtomic) , ReadableProperty('effectivePeriod', DateRange) - , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) + , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule, 7)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index 2d9557be..dfbc5742 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -1155,8 +1155,7 @@ def get_long(self): def keylist(self): """Return a list of names in order by value.""" - items = self.enumerations.items() - items.sort(lambda a, b: self.cmp(a[1], b[1])) + items = sorted(self.enumerations.items(), key=lambda a: a[1]) # last item has highest value rslt = [None] * (items[-1][1] + 1) diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index f6ec6fd0..25ffdf1c 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -7,11 +7,10 @@ from ..primitivedata import Atomic, Null, Unsigned from ..constructeddata import Any, Array, ArrayOf, List -from ..apdu import Error, \ - SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ +from ..apdu import SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import Property, Object, PropertyError +from ..object import PropertyError # some debugging _debug = 0 @@ -62,7 +61,7 @@ def do_ReadPropertyRequest(self, apdu): raise PropertyError(apdu.propertyIdentifier) # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): if apdu.propertyArrayIndex == 0: @@ -164,7 +163,7 @@ def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): raise ExecutionError(errorClass='property', errorCode='unknownProperty') # change atomic values into something encodeable - if issubclass(datatype, Atomic): + if issubclass(datatype, Atomic) or (issubclass(datatype, (Array, List)) and isinstance(value, list)): value = datatype(value) elif issubclass(datatype, Array) and (propertyArrayIndex is not None): if propertyArrayIndex == 0: diff --git a/samples/COVClient.py b/samples/COVClient.py index be02d1f4..41673e79 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -15,8 +15,7 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.apdu import SubscribeCOVRequest, \ - SimpleAckPDU, RejectPDU, AbortPDU +from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU, RejectPDU, AbortPDU from bacpypes.primitivedata import ObjectIdentifier from bacpypes.app import BIPSimpleApplication @@ -36,68 +35,84 @@ # SubscribeCOVApplication # + @bacpypes_debugging class SubscribeCOVApplication(BIPSimpleApplication): - def __init__(self, *args): - if _debug: SubscribeCOVApplication._debug("__init__ %r", args) + if _debug: + SubscribeCOVApplication._debug("__init__ %r", args) BIPSimpleApplication.__init__(self, *args) def do_ConfirmedCOVNotificationRequest(self, apdu): - if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + if _debug: + SubscribeCOVApplication._debug( + "do_ConfirmedCOVNotificationRequest %r", apdu + ) global rsvp - print("{} changed\n {}".format( - apdu.monitoredObjectIdentifier, - ",\n ".join("{} = {}".format( - element.propertyIdentifier, - str(element.value), - ) for element in apdu.listOfValues), - )) + print("{} changed\n".format(apdu.monitoredObjectIdentifier)) + for element in apdu.listOfValues: + element_value = element.value.tagList + if _debug: + SubscribeCOVApplication._debug(" - propertyIdentifier: %r", element.propertyIdentifier) + SubscribeCOVApplication._debug(" - value tag list: %r", element_value) + + if len(element_value) == 1: + element_value = element_value[0].app_to_object().value + + print(" {} is {}".format(element.propertyIdentifier, str(element_value))) if rsvp[0]: # success response = SimpleAckPDU(context=apdu) - if _debug: SubscribeCOVApplication._debug(" - simple_ack: %r", response) + if _debug: + SubscribeCOVApplication._debug(" - simple_ack: %r", response) elif rsvp[1]: # reject response = RejectPDU(reason=rsvp[1], context=apdu) - if _debug: SubscribeCOVApplication._debug(" - reject: %r", response) + if _debug: + SubscribeCOVApplication._debug(" - reject: %r", response) elif rsvp[2]: # abort response = AbortPDU(reason=rsvp[2], context=apdu) - if _debug: SubscribeCOVApplication._debug(" - abort: %r", response) + if _debug: + SubscribeCOVApplication._debug(" - abort: %r", response) # return the result self.response(response) def do_UnconfirmedCOVNotificationRequest(self, apdu): - if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + if _debug: + SubscribeCOVApplication._debug( + "do_UnconfirmedCOVNotificationRequest %r", apdu + ) + + print("{} changed\n".format(apdu.monitoredObjectIdentifier)) + for element in apdu.listOfValues: + element_value = element.value.tagList + if len(element_value) == 1: + element_value = element_value[0].app_to_object().value + + print(" {} is {}".format(element.propertyIdentifier, str(element_value))) - print("{} changed\n {}".format( - apdu.monitoredObjectIdentifier, - ",\n ".join("{} is {}".format( - element.propertyIdentifier, - str(element.value), - ) for element in apdu.listOfValues), - )) # # SubscribeCOVConsoleCmd # + @bacpypes_debugging class SubscribeCOVConsoleCmd(ConsoleCmd): - def do_subscribe(self, args): """subscribe addr proc_id obj_id [ confirmed ] [ lifetime ] Generate a SubscribeCOVRequest and wait for the response. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + if _debug: + SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) try: addr, proc_id, obj_id = args[:3] @@ -107,29 +122,32 @@ def do_subscribe(self, args): if len(args) >= 4: issue_confirmed = args[3] - if issue_confirmed == '-': + if issue_confirmed == "-": issue_confirmed = None else: - issue_confirmed = issue_confirmed.lower() == 'true' - if _debug: SubscribeCOVConsoleCmd._debug(" - issue_confirmed: %r", issue_confirmed) + issue_confirmed = issue_confirmed.lower() == "true" + if _debug: + SubscribeCOVConsoleCmd._debug( + " - issue_confirmed: %r", issue_confirmed + ) else: issue_confirmed = None if len(args) >= 5: lifetime = args[4] - if lifetime == '-': + if lifetime == "-": lifetime = None else: lifetime = int(lifetime) - if _debug: SubscribeCOVConsoleCmd._debug(" - lifetime: %r", lifetime) + if _debug: + SubscribeCOVConsoleCmd._debug(" - lifetime: %r", lifetime) else: lifetime = None # build a request request = SubscribeCOVRequest( - subscriberProcessIdentifier=proc_id, - monitoredObjectIdentifier=obj_id, - ) + subscriberProcessIdentifier=proc_id, monitoredObjectIdentifier=obj_id + ) request.pduDestination = Address(addr) # optional parameters @@ -138,11 +156,13 @@ def do_subscribe(self, args): if lifetime is not None: request.lifetime = lifetime - if _debug: SubscribeCOVConsoleCmd._debug(" - request: %r", request) + if _debug: + SubscribeCOVConsoleCmd._debug(" - request: %r", request) # make an IOCB iocb = IOCB(request) - if _debug: SubscribeCOVConsoleCmd._debug(" - iocb: %r", iocb) + if _debug: + SubscribeCOVConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application deferred(this_application.request_io, iocb) @@ -152,11 +172,13 @@ def do_subscribe(self, args): # do something for success if iocb.ioResponse: - if _debug: SubscribeCOVConsoleCmd._debug(" - response: %r", iocb.ioResponse) + if _debug: + SubscribeCOVConsoleCmd._debug(" - response: %r", iocb.ioResponse) # do something for error/reject/abort if iocb.ioError: - if _debug: SubscribeCOVConsoleCmd._debug(" - error: %r", iocb.ioError) + if _debug: + SubscribeCOVConsoleCmd._debug(" - error: %r", iocb.ioError) except Exception as e: SubscribeCOVConsoleCmd._exception("exception: %r", e) @@ -168,7 +190,8 @@ def do_ack(self, args): simple acknowledgement. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_ack %r", args) + if _debug: + SubscribeCOVConsoleCmd._debug("do_ack %r", args) global rsvp rsvp = (True, None, None) @@ -180,7 +203,8 @@ def do_reject(self, args): reject PDU with the provided reason. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_reject %r", args) + if _debug: + SubscribeCOVConsoleCmd._debug("do_reject %r", args) global rsvp rsvp = (False, args[0], None) @@ -192,7 +216,8 @@ def do_abort(self, args): abort PDU with the provided reason. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_abort %r", args) + if _debug: + SubscribeCOVConsoleCmd._debug("do_abort %r", args) global rsvp rsvp = (False, None, args[0]) @@ -202,25 +227,30 @@ def do_abort(self, args): # __main__ # + def main(): global this_application # parse the command line arguments args = ConfigArgumentParser(description=__doc__).parse_args() - if _debug: _log.debug("initialization") - if _debug: _log.debug(" - args: %r", args) + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) # make a device object this_device = LocalDeviceObject(ini=args.ini) - if _debug: _log.debug(" - this_device: %r", this_device) + if _debug: + _log.debug(" - this_device: %r", this_device) # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) # make a console this_console = SubscribeCOVConsoleCmd() - if _debug: _log.debug(" - this_console: %r", this_console) + if _debug: + _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() @@ -231,5 +261,6 @@ def main(): _log.debug("fini") + if __name__ == "__main__": main() diff --git a/samples/DiscoverToDo.py b/samples/DiscoverToDo.py new file mode 100644 index 00000000..ad1c8f29 --- /dev/null +++ b/samples/DiscoverToDo.py @@ -0,0 +1,1626 @@ +#!/usr/bin/python3 + +""" +Autodiscover + +This is an application to assist with the "discovery" process of finding +BACnet routers, devices, objects, and property values. It reads and/or writes +a tab-delimited property values text file of the values that it has received. +""" + +import sys +import time +import json + +from collections import defaultdict, OrderedDict + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.pdu import Address, LocalBroadcast, GlobalBroadcast +from bacpypes.comm import bind +from bacpypes.core import run, deferred, enable_sleeping +from bacpypes.task import FunctionTask +from bacpypes.iocb import IOCB, IOQController + +# application layer +from bacpypes.primitivedata import Unsigned, ObjectIdentifier +from bacpypes.constructeddata import Array, ArrayOf +from bacpypes.basetypes import PropertyIdentifier, ServicesSupported +from bacpypes.object import get_object_class, get_datatype, DeviceObject + +from bacpypes.app import ApplicationIOController +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.apdu import ( + WhoIsRequest, + IAmRequest, + ReadPropertyRequest, + ReadPropertyACK, + ReadPropertyMultipleRequest, + PropertyReference, + ReadAccessSpecification, + ReadPropertyMultipleACK, +) + +# network layer +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.npdu import ( + WhoIsRouterToNetwork, + IAmRouterToNetwork, + InitializeRoutingTable, + InitializeRoutingTableAck, + WhatIsNetworkNumber, + NetworkNumberIs, +) + +# IPv4 virtual link layer +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +# basic objects +from bacpypes.local.device import LocalDeviceObject + +# basic services +from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_device = None +this_application = None +snapshot = None + +# device information +device_profile = defaultdict(DeviceObject) + +# print statements just for interactive +interactive = sys.stdin.isatty() + +# lists of things to do +network_path_to_do_list = None +who_is_to_do_list = None +application_to_do_list = None + +# +# Snapshot +# + + +@bacpypes_debugging +class Snapshot: + def __init__(self): + if _debug: + Snapshot._debug("__init__") + + # empty database + self.data = {} + + def read_file(self, filename): + if _debug: + Snapshot._debug("read_file %r", filename) + + # empty database + self.data = {} + + try: + with open(filename) as infile: + lines = infile.readlines() + for line in lines: + devid, objid, propid, version, value = line[:-1].split("\t") + + devid = int(devid) + version = int(version) + + key = (devid, objid, propid) + self.data[key] = (version, value) + except IOError: + if _debug: + Snapshot._debug(" - file not found") + pass + + def write_file(self, filename): + if _debug: + Snapshot._debug("write_file %r", filename) + + data = list(k + v for k, v in self.data.items()) + data.sort() + + with open(filename, "w") as outfile: + for row in data: + outfile.write("\t".join(str(x) for x in row) + "\n") + + def upsert(self, devid, objid, propid, value): + if _debug: + Snapshot._debug("upsert %r %r %r %r", devid, objid, propid, value) + + key = (devid, objid, propid) + if key not in self.data: + if _debug: + Snapshot._debug(" - new key") + self.data[key] = (1, value) + else: + version, old_value = self.data[key] + if value != old_value: + if _debug: + Snapshot._debug(" - new value") + self.data[key] = (version + 1, value) + + def get_value(self, devid, objid, propid): + if _debug: + Snapshot._debug("get_value %r %r %r", devid, objid, propid) + + key = (devid, objid, propid) + if key not in self.data: + return None + else: + return self.data[key][1] + + +# +# ToDoItem +# + + +@bacpypes_debugging +class ToDoItem: + def __init__(self, _thread=None, _delay=None): + if _debug: + ToDoItem._debug("__init__") + + # basic status information + self._completed = False + + # may depend on another item to complete, may have a delay + self._thread = _thread + self._delay = _delay + + def prepare(self): + if _debug: + ToDoItem._debug("prepare") + raise NotImplementedError + + def complete(self, iocb): + if _debug: + ToDoItem._debug("complete %r", iocb) + self._completed = True + + +# +# ToDoList +# + + +@bacpypes_debugging +class ToDoList: + def __init__(self, controller, active_limit=1): + if _debug: + ToDoList._debug("__init__") + + # save a reference to the controller for workers + self.controller = controller + + # limit to the number of active workers + self.active_limit = active_limit + + # no workers, nothing active + self.pending = [] + self.active = set() + + # launch already deferred + self.launch_deferred = False + + def append(self, item): + if _debug: + ToDoList._debug("append %r", item) + + # add the item to the list of pending items + self.pending.append(item) + + # if an item can be started, schedule to launch it + if len(self.active) < self.active_limit and not self.launch_deferred: + if _debug: + ToDoList._debug(" - will launch") + + self.launch_deferred = True + deferred(self.launch) + + def launch(self): + if _debug: + ToDoList._debug("launch") + + # find some workers and launch them + while self.pending and (len(self.active) < self.active_limit): + # look for the next to_do_item that can be started + for i, item in enumerate(self.pending): + if not item._thread: + break + if item._thread._completed: + break + else: + if _debug: + ToDoList._debug(" - waiting") + break + if _debug: + ToDoList._debug(" - item: %r", item) + + # remove it from the pending list, add it to active + del self.pending[i] + self.active.add(item) + + # prepare it and capture the IOCB + iocb = item.prepare() + if _debug: + ToDoList._debug(" - iocb: %r", iocb) + + # break the reference to the completed to_do_item + item._thread = None + iocb._to_do_item = item + + # add our completion routine + iocb.add_callback(self.complete) + + # submit it to our controller + self.controller.request_io(iocb) + + # clear the deferred flag + self.launch_deferred = False + if _debug: + ToDoList._debug(" - done launching") + + # check for idle + if (not self.active) and (not self.pending): + self.idle() + + def complete(self, iocb): + if _debug: + ToDoList._debug("complete %r", iocb) + + # extract the to_do_item + item = iocb._to_do_item + if _debug: + ToDoList._debug(" - item: %r", item) + + # if the item has a delay, schedule to call it later + if item._delay: + task = FunctionTask(self._delay_complete, item, iocb) + task.install_task(delta=item._delay) + if _debug: + ToDoList._debug(" - task: %r", task) + else: + self._delay_complete(item, iocb) + + def _delay_complete(self, item, iocb): + if _debug: + ToDoList._debug("_delay_complete %r %r", item, iocb) + + # tell the item it completed, remove it from active + item.complete(iocb) + self.active.remove(item) + + # find another to_do_item + if not self.launch_deferred: + if _debug: + ToDoList._debug(" - will launch") + + self.launch_deferred = True + deferred(self.launch) + + def idle(self): + if _debug: + ToDoList._debug("idle") + + +# +# DiscoverNetworkServiceElement +# + + +@bacpypes_debugging +class DiscoverNetworkServiceElement(NetworkServiceElement, IOQController): + def __init__(self): + if _debug: + DiscoverNetworkServiceElement._debug("__init__") + NetworkServiceElement.__init__(self) + IOQController.__init__(self) + + def process_io(self, iocb): + if _debug: + DiscoverNetworkServiceElement._debug("process_io %r", iocb) + + # this request is active + self.active_io(iocb) + + # reference the service access point + sap = self.elementService + if _debug: + NetworkServiceElement._debug(" - sap: %r", sap) + + # the iocb contains an NPDU, pass it along to the local adapter + self.request(sap.local_adapter, iocb.args[0]) + + def indication(self, adapter, npdu): + if _debug: + DiscoverNetworkServiceElement._debug("indication %r %r", adapter, npdu) + global network_path_to_do_list + + if not self.active_iocb: + pass + + elif isinstance(npdu, IAmRouterToNetwork): + if interactive: + print("{} router to {}".format(npdu.pduSource, npdu.iartnNetworkList)) + + # reference the request + request = self.active_iocb.args[0] + if isinstance(request, WhoIsRouterToNetwork): + if request.wirtnNetwork in npdu.iartnNetworkList: + self.complete_io(self.active_iocb, npdu.pduSource) + + elif isinstance(npdu, InitializeRoutingTableAck): + if interactive: + print("{} routing table".format(npdu.pduSource)) + for rte in npdu.irtaTable: + print( + " {} {} {}".format(rte.rtDNET, rte.rtPortID, rte.rtPortInfo) + ) + + # reference the request + request = self.active_iocb.args[0] + if isinstance(request, InitializeRoutingTable): + if npdu.pduSource == request.pduDestination: + self.complete_io(self.active_iocb, npdu.irtaTable) + + elif isinstance(npdu, NetworkNumberIs): + if interactive: + print("{} network number is {}".format(npdu.pduSource, npdu.nniNet)) + + # reference the request + request = self.active_iocb.args[0] + if isinstance(request, WhatIsNetworkNumber): + self.complete_io(self.active_iocb, npdu.nniNet) + + # forward it along + NetworkServiceElement.indication(self, adapter, npdu) + + +# +# DiscoverApplication +# + + +@bacpypes_debugging +class DiscoverApplication( + ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices +): + def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + if _debug: + DiscoverApplication._debug( + "__init__ %r %r deviceInfoCache=%r aseID=%r", + localDevice, + localAddress, + deviceInfoCache, + aseID, + ) + ApplicationIOController.__init__( + self, localDevice, localAddress, deviceInfoCache, aseID=aseID + ) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(localDevice) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = DiscoverNetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a generic BIP stack, bound to the Annex J server + # on the UDP multiplexer + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(self.localAddress) + + # bind the bottom layers + bind(self.bip, self.annexj, self.mux.annexJ) + + # bind the BIP stack to the network, no network number + self.nsap.bind(self.bip, address=self.localAddress) + + def close_socket(self): + if _debug: + DiscoverApplication._debug("close_socket") + + # pass to the multiplexer, then down to the sockets + self.mux.close_socket() + + def do_IAmRequest(self, apdu): + if _debug: + DiscoverApplication._debug("do_IAmRequest %r", apdu) + global who_is_to_do_list + + # pass it along to line up with active requests + who_is_to_do_list.received_i_am(apdu) + + +# +# WhoIsToDo +# + + +@bacpypes_debugging +class WhoIsToDo(ToDoItem): + def __init__(self, addr, lolimit, hilimit): + if _debug: + WhoIsToDo._debug("__init__ %r %r %r", addr, lolimit, hilimit) + ToDoItem.__init__(self, _delay=3.0) + + # save the parameters + self.addr = addr + self.lolimit = lolimit + self.hilimit = hilimit + + # hold on to the request and make a placeholder for responses + self.request = None + self.i_am_responses = [] + + # give it to the list + who_is_to_do_list.append(self) + + def prepare(self): + if _debug: + WhoIsToDo._debug("prepare(%r %r %r)", self.addr, self.lolimit, self.hilimit) + + # build a request + self.request = WhoIsRequest( + destination=self.addr, + deviceInstanceRangeLowLimit=self.lolimit, + deviceInstanceRangeHighLimit=self.hilimit, + ) + if _debug: + WhoIsToDo._debug(" - request: %r", self.request) + + # build an IOCB + iocb = IOCB(self.request) + if _debug: + WhoIsToDo._debug(" - iocb: %r", iocb) + + return iocb + + def complete(self, iocb): + if _debug: + WhoIsToDo._debug("complete %r", iocb) + + # process the responses + for apdu in self.i_am_responses: + device_instance = apdu.iAmDeviceIdentifier[1] + + # print out something + if interactive: + print("{} @ {}".format(device_instance, apdu.pduSource)) + + # update the snapshot database + snapshot.upsert(device_instance, "-", "address", str(apdu.pduSource)) + snapshot.upsert( + device_instance, + "-", + "maxAPDULengthAccepted", + str(apdu.maxAPDULengthAccepted), + ) + snapshot.upsert( + device_instance, + "-", + "segmentationSupported", + apdu.segmentationSupported, + ) + + # read stuff + ReadServicesSupported(device_instance) + ReadObjectList(device_instance) + + # pass along + ToDoItem.complete(self, iocb) + + +# +# WhoIsToDoList +# + + +@bacpypes_debugging +class WhoIsToDoList(ToDoList): + def received_i_am(self, apdu): + if _debug: + WhoIsToDoList._debug("received_i_am %r", apdu) + + # line it up with an active item + for item in self.active: + if _debug: + WhoIsToDoList._debug(" - item: %r", item) + + # check the source against the request + if item.addr.addrType == Address.localBroadcastAddr: + if apdu.pduSource.addrType != Address.localStationAddr: + if _debug: + WhoIsToDoList._debug(" - not a local station") + continue + + elif item.addr.addrType == Address.localStationAddr: + if apdu.pduSource != item.addr: + if _debug: + WhoIsToDoList._debug(" - not from station") + continue + + elif item.addr.addrType == Address.remoteBroadcastAddr: + if apdu.pduSource.addrType != Address.remoteStationAddr: + if _debug: + WhoIsToDoList._debug(" - not a remote station") + continue + if apdu.pduSource.addrNet != item.addr.addrNet: + if _debug: + WhoIsToDoList._debug(" - not from remote net") + continue + + elif item.addr.addrType == Address.remoteStationAddr: + if apdu.pduSource != item.addr: + if _debug: + WhoIsToDoList._debug(" - not correct remote station") + continue + + # check the range restriction + device_instance = apdu.iAmDeviceIdentifier[1] + if (item.lolimit is not None) and (device_instance < item.lolimit): + if _debug: + WhoIsToDoList._debug(" - lo limit") + continue + if (item.hilimit is not None) and (device_instance > item.hilimit): + if _debug: + WhoIsToDoList._debug(" - hi limit") + continue + + # debug in case something kicked it out + if _debug: + WhoIsToDoList._debug(" - passed") + + # save this response + item.i_am_responses.append(apdu) + + def idle(self): + if _debug: + WhoIsToDoList._debug("idle") + + +# +# ApplicationToDoList +# + + +@bacpypes_debugging +class ApplicationToDoList(ToDoList): + def __init__(self): + if _debug: + ApplicationToDoList._debug("__init__") + global this_application + + ToDoList.__init__(self, this_application) + + +# +# ReadPropertyToDo +# + + +@bacpypes_debugging +class ReadPropertyToDo(ToDoItem): + def __init__(self, devid, objid, propid, index=None): + if _debug: + ReadPropertyToDo._debug( + "__init__ %r %r %r index=%r", devid, objid, propid, index + ) + ToDoItem.__init__(self) + + # save the parameters + self.devid = devid + self.objid = objid + self.propid = propid + self.index = index + + # give it to the list + application_to_do_list.append(self) + + def prepare(self): + if _debug: + ReadPropertyToDo._debug( + "prepare(%r %r %r)", self.devid, self.objid, self.propid + ) + + # map the devid identifier to an address from the database + addr = snapshot.get_value(self.devid, "-", "address") + if not addr: + raise ValueError("unknown device") + if _debug: + ReadPropertyToDo._debug(" - addr: %r", addr) + + # build a request + request = ReadPropertyRequest( + destination=Address(addr), + objectIdentifier=self.objid, + propertyIdentifier=self.propid, + ) + + if self.index is not None: + request.propertyArrayIndex = self.index + if _debug: + ReadPropertyToDo._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: + ReadPropertyToDo._debug(" - iocb: %r", iocb) + + return iocb + + def complete(self, iocb): + if _debug: + ReadPropertyToDo._debug("complete %r", iocb) + + # do something for error/reject/abort + if iocb.ioError: + if interactive: + print("{} error: {}".format(self.propid, iocb.ioError)) + + # do something more + self.returned_error(iocb.ioError) + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: + ReadPropertyToDo._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: + ReadPropertyToDo._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: + ReadPropertyToDo._debug(" - datatype: %r", datatype) + + try: + value = apdu.propertyValue.cast_out(datatype) + if _debug: + ReadPropertyToDo._debug(" - value: %r", value) + + # convert the value to a string + if hasattr(value, "dict_contents"): + dict_contents = value.dict_contents(as_class=OrderedDict) + str_value = json.dumps(dict_contents) + else: + str_value = str(value) + except Exception as err: + str_value = "!" + str(err) + + if interactive: + print(str_value) + + # make a pretty property identifier + str_prop = apdu.propertyIdentifier + if apdu.propertyArrayIndex is not None: + str_prop += "[{}]".format(apdu.propertyArrayIndex) + + # save it in the snapshot + snapshot.upsert( + self.devid, "{}:{}".format(*apdu.objectIdentifier), str_prop, str_value + ) + + # do something more + self.returned_value(value) + + # do something with nothing? + else: + if _debug: + ReadPropertyToDo._debug(" - ioError or ioResponse expected") + + def returned_error(self, error): + if _debug: + ReadPropertyToDo._debug("returned_error %r", error) + + def returned_value(self, value): + if _debug: + ReadPropertyToDo._debug("returned_value %r", value) + + +# +# ReadServicesSupported +# + + +@bacpypes_debugging +class ReadServicesSupported(ReadPropertyToDo): + def __init__(self, devid): + if _debug: + ReadServicesSupported._debug("__init__ %r", devid) + ReadPropertyToDo.__init__( + self, devid, ("device", devid), "protocolServicesSupported" + ) + + def returned_value(self, value): + if _debug: + ReadServicesSupported._debug("returned_value %r", value) + + # build a value + services_supported = ServicesSupported(value) + print( + "{} supports rpm: {}".format( + self.devid, services_supported["readPropertyMultiple"] + ) + ) + + # device profile + devobj = device_profile[self.devid] + devobj.protocolServicesSupported = services_supported + + +# +# ReadObjectList +# + + +@bacpypes_debugging +class ReadObjectList(ReadPropertyToDo): + def __init__(self, devid): + if _debug: + ReadObjectList._debug("__init__ %r", devid) + ReadPropertyToDo.__init__(self, devid, ("device", devid), "objectList") + + def returned_error(self, error): + if _debug: + ReadObjectList._debug("returned_error %r", error) + + # try reading the length of the list + ReadObjectListLen(self.devid) + + def returned_value(self, value): + if _debug: + ReadObjectList._debug("returned_value %r", value) + + # update the device profile + devobj = device_profile[self.devid] + devobj.objectList = ArrayOf(ObjectIdentifier)(value) + + # read the properties of the objects + for objid in value: + ReadObjectProperties(self.devid, objid) + + +# +# ReadPropertyMultipleToDo +# + + +@bacpypes_debugging +class ReadPropertyMultipleToDo(ToDoItem): + def __init__(self, devid, objid, proplist): + if _debug: + ReadPropertyMultipleToDo._debug("__init__ %r %r %r", devid, objid, proplist) + ToDoItem.__init__(self) + + # save the parameters + self.devid = devid + self.objid = objid + self.proplist = proplist + + # give it to the list + application_to_do_list.append(self) + + def prepare(self): + if _debug: + ReadPropertyMultipleToDo._debug( + "prepare(%r %r %r)", self.devid, self.objid, self.proplist + ) + + # map the devid identifier to an address from the database + addr = snapshot.get_value(self.devid, "-", "address") + if not addr: + raise ValueError("unknown device") + if _debug: + ReadPropertyMultipleToDo._debug(" - addr: %r", addr) + + prop_reference_list = [ + PropertyReference(propertyIdentifier=propid) for propid in self.proplist + ] + + # build a read access specification + read_access_spec = ReadAccessSpecification( + objectIdentifier=self.objid, listOfPropertyReferences=prop_reference_list + ) + + # build the request + request = ReadPropertyMultipleRequest( + destination=Address(addr), listOfReadAccessSpecs=[read_access_spec] + ) + if _debug: + ReadPropertyMultipleToDo._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: + ReadPropertyMultipleToDo._debug(" - iocb: %r", iocb) + + return iocb + + def complete(self, iocb): + if _debug: + ReadPropertyMultipleToDo._debug("complete %r", iocb) + + # do something for error/reject/abort + if iocb.ioError: + if interactive: + print(str(iocb.ioError)) + + # do something more + self.returned_error(iocb.ioError) + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyMultipleACK): + if _debug: + ReadPropertyMultipleToDo._debug(" - not an ack") + return + + # loop through the results + for result in apdu.listOfReadAccessResults: + # here is the object identifier + objectIdentifier = result.objectIdentifier + if _debug: + ReadPropertyMultipleToDo._debug( + " - objectIdentifier: %r", objectIdentifier + ) + + # now come the property values per object + for element in result.listOfResults: + # get the property and array index + propertyIdentifier = element.propertyIdentifier + if _debug: + ReadPropertyMultipleToDo._debug( + " - propertyIdentifier: %r", propertyIdentifier + ) + propertyArrayIndex = element.propertyArrayIndex + if _debug: + ReadPropertyMultipleToDo._debug( + " - propertyArrayIndex: %r", propertyArrayIndex + ) + + # here is the read result + readResult = element.readResult + + property_label = str(propertyIdentifier) + if propertyArrayIndex is not None: + property_label += "[" + str(propertyArrayIndex) + "]" + + # check for an error + if readResult.propertyAccessError is not None: + if interactive: + print( + "{} ! {}".format( + property_label, readResult.propertyAccessError + ) + ) + + else: + # here is the value + propertyValue = readResult.propertyValue + + # find the datatype + datatype = get_datatype(objectIdentifier[0], propertyIdentifier) + if _debug: + ReadPropertyMultipleToDo._debug( + " - datatype: %r", datatype + ) + if not datatype: + str_value = "?" + else: + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and ( + propertyArrayIndex is not None + ): + if propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: + ReadPropertyMultipleToDo._debug( + " - datatype: %r", datatype + ) + + try: + value = propertyValue.cast_out(datatype) + if _debug: + ReadPropertyMultipleToDo._debug( + " - value: %r", value + ) + + # convert the value to a string + if hasattr(value, "dict_contents"): + dict_contents = value.dict_contents( + as_class=OrderedDict + ) + str_value = json.dumps(dict_contents) + else: + str_value = str(value) + except Exception as err: + str_value = "!" + str(err) + + if interactive: + print("{}: {}".format(propertyIdentifier, str_value)) + + # save it in the snapshot + snapshot.upsert( + self.devid, + "{}:{}".format(*objectIdentifier), + str(propertyIdentifier), + str_value, + ) + + # do something with nothing? + else: + if _debug: + ReadPropertyMultipleToDo._debug(" - ioError or ioResponse expected") + + def returned_error(self, error): + if _debug: + ReadPropertyMultipleToDo._debug("returned_error %r", error) + + def returned_value(self, value): + if _debug: + ReadPropertyMultipleToDo._debug("returned_value %r", value) + + +# +# ReadObjectListLen +# + + +@bacpypes_debugging +class ReadObjectListLen(ReadPropertyToDo): + def __init__(self, devid): + if _debug: + ReadObjectListLen._debug("__init__ %r", devid) + ReadPropertyToDo.__init__(self, devid, ("device", devid), "objectList", 0) + + def returned_error(self, error): + if _debug: + ReadObjectListLen._debug("returned_error %r", error) + + def returned_value(self, value): + if _debug: + ReadObjectListLen._debug("returned_value %r", value) + + # start with an empty list + devobj = device_profile[self.devid] + devobj.objectList = ArrayOf(ObjectIdentifier)() + + # read each of the individual items + for i in range(1, value + 1): + ReadObjectListElement(self.devid, i) + + +# +# ReadObjectListElement +# + + +@bacpypes_debugging +class ReadObjectListElement(ReadPropertyToDo): + def __init__(self, devid, indx): + if _debug: + ReadObjectListElement._debug("__init__ %r", devid, indx) + ReadPropertyToDo.__init__(self, devid, ("device", devid), "objectList", indx) + + def returned_error(self, error): + if _debug: + ReadObjectListElement._debug("returned_error %r", error) + + def returned_value(self, value): + if _debug: + ReadObjectListElement._debug("returned_value %r", value) + + # update the list + devobj = device_profile[self.devid] + devobj.objectList.append(value) + + # read the properties of the object + ReadObjectProperties(self.devid, value) + + +# +# ReadObjectProperties +# + + +@bacpypes_debugging +def ReadObjectProperties(devid, objid): + if _debug: + ReadObjectProperties._debug("ReadObjectProperties %r %r", devid, objid) + + # get the profile, it contains the protocol services supported + devobj = device_profile[devid] + supports_rpm = devobj.protocolServicesSupported["readPropertyMultiple"] + if _debug: + ReadObjectProperties._debug(" - supports rpm: %r", supports_rpm) + + # read all the properties at once if it's an option + if supports_rpm: + ReadPropertyMultipleToDo(devid, objid, ["all"]) + else: + ReadObjectPropertyList(devid, objid) + + +# +# ReadObjectPropertyList +# + + +@bacpypes_debugging +class ReadObjectPropertyList(ReadPropertyToDo): + def __init__(self, devid, objid): + if _debug: + ReadObjectPropertyList._debug("__init__ %r", devid) + ReadPropertyToDo.__init__(self, devid, objid, "propertyList") + + def returned_error(self, error): + if _debug: + ReadObjectPropertyList._debug("returned_error %r", error) + + # get the class + object_class = get_object_class(self.objid[0]) + if _debug: + ReadObjectPropertyList._debug(" - object_class: %r", object_class) + + # get a list of properties, including optional ones + object_properties = list(object_class._properties.keys()) + if _debug: + ReadObjectPropertyList._debug( + " - object_properties: %r", object_properties + ) + + # dont bother reading the property list, it already failed + if "propertyList" in object_properties: + object_properties.remove("propertyList") + + # try to read all the properties + for propid in object_properties: + ReadPropertyToDo(self.devid, self.objid, propid) + + def returned_value(self, value): + if _debug: + ReadObjectPropertyList._debug("returned_value %r", value) + + # add the other properties that are always present but not + # returned in property list + value.extend(("objectName", "objectType", "objectIdentifier")) + + # read each of the individual properties + for propid in value: + ReadPropertyToDo(self.devid, self.objid, propid) + + +# +# DiscoverConsoleCmd +# + + +@bacpypes_debugging +class DiscoverConsoleCmd(ConsoleCmd): + def do_sleep(self, args): + """ + sleep + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_sleep %r", args) + + time.sleep(float(args[0])) + + def do_wirtn(self, args): + """ + wirtn [ ] [ ] + + Send a Who-Is-Router-To-Network message. If is not specified + the message is locally broadcast. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_wirtn %r", args) + + # build a request + try: + request = WhoIsRouterToNetwork() + if not args: + request.pduDestination = LocalBroadcast() + elif args[0].isdigit(): + request.pduDestination = LocalBroadcast() + request.wirtnNetwork = int(args[0]) + else: + request.pduDestination = Address(args[0]) + if len(args) > 1: + request.wirtnNetwork = int(args[1]) + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + # sleep for responses + time.sleep(3.0) + + def do_irt(self, args): + """ + irt + + Send an empty Initialize-Routing-Table message to an address, a router + will return an acknowledgement with its routing table configuration. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_irt %r", args) + + # build a request + try: + request = InitializeRoutingTable() + request.pduDestination = Address(args[0]) + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + def do_winn(self, args): + """ + winn [ ] + + Send a What-Is-Network-Number message. If the address is unspecified + the message is locally broadcast. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_winn %r", args) + + # build a request + try: + request = WhatIsNetworkNumber() + if len(args) > 0: + request.pduDestination = Address(args[0]) + else: + request.pduDestination = LocalBroadcast() + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + # sleep for responses + time.sleep(3.0) + + def do_whois(self, args): + """ + whois [ ] [ ] + + Send a Who-Is Request and wait 3 seconds for the I-Am "responses" to + be returned. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_whois %r", args) + + try: + # parse parameters + if (len(args) == 1) or (len(args) == 3): + addr = Address(args[0]) + del args[0] + else: + addr = GlobalBroadcast() + if len(args) == 2: + lolimit = int(args[0]) + hilimit = int(args[1]) + else: + lolimit = hilimit = None + + # make an item + item = WhoIsToDo(addr, lolimit, hilimit) + if _debug: + DiscoverConsoleCmd._debug(" - item: %r", item) + + except Exception as err: + DiscoverConsoleCmd._exception("exception: %r", err) + + def do_iam(self, args): + """ + iam [ ] + + Send an I-Am request. If the address is unspecified the message is + locally broadcast. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_iam %r", args) + + try: + # build a request + request = IAmRequest() + if len(args) == 1: + request.pduDestination = Address(args[0]) + else: + request.pduDestination = GlobalBroadcast() + + # set the parameters from the device object + request.iAmDeviceIdentifier = this_device.objectIdentifier + request.maxAPDULengthAccepted = this_device.maxApduLengthAccepted + request.segmentationSupported = this_device.segmentationSupported + request.vendorID = this_device.vendorIdentifier + if _debug: + DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: + DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + except Exception as err: + DiscoverConsoleCmd._exception("exception: %r", err) + + def do_rp(self, args): + """ + rp [ ] + + Send a Read-Property request to a device identified by its device + identifier. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_rp %r", args) + global application_to_do_list + + try: + devid, objid, propid = args[:3] + + devid = int(devid) + objid = ObjectIdentifier(objid).value + + datatype = get_datatype(objid[0], propid) + if not datatype: + raise ValueError("invalid property for object type") + + if len(args) == 4: + index = int(args[3]) + else: + index = None + + # make something to do + ReadPropertyToDo(devid, objid, propid, index) + + except Exception as error: + DiscoverConsoleCmd._exception("exception: %r", error) + + def do_rpm(self, args): + """ + rpm ( ( [ ] )... )... + + Send a Read-Property-Multiple request to a device identified by its + device identifier. + """ + args = args.split() + if _debug: + DiscoverConsoleCmd._debug("do_rpm %r", args) + + try: + i = 0 + devid = int(args[i]) + if _debug: + DiscoverConsoleCmd._debug(" - devid: %r", devid) + i += 1 + + # map the devid identifier to an address from the database + addr = snapshot.get_value(devid, "-", "address") + if not addr: + raise ValueError("unknown device") + if _debug: + DiscoverConsoleCmd._debug(" - addr: %r", addr) + + read_access_spec_list = [] + while i < len(args): + obj_id = ObjectIdentifier(args[i]).value + if _debug: + DiscoverConsoleCmd._debug(" - obj_id: %r", obj_id) + i += 1 + + prop_reference_list = [] + while i < len(args): + prop_id = args[i] + if _debug: + DiscoverConsoleCmd._debug(" - prop_id: %r", prop_id) + if prop_id not in PropertyIdentifier.enumerations: + break + + i += 1 + if prop_id in ("all", "required", "optional"): + pass + else: + datatype = get_datatype(obj_id[0], prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a property reference + prop_reference = PropertyReference(propertyIdentifier=prop_id) + + # check for an array index + if (i < len(args)) and args[i].isdigit(): + prop_reference.propertyArrayIndex = int(args[i]) + i += 1 + + # add it to the list + prop_reference_list.append(prop_reference) + + # check for at least one property + if not prop_reference_list: + raise ValueError("provide at least one property") + + # build a read access specification + read_access_spec = ReadAccessSpecification( + objectIdentifier=obj_id, + listOfPropertyReferences=prop_reference_list, + ) + + # add it to the list + read_access_spec_list.append(read_access_spec) + + # check for at least one + if not read_access_spec_list: + raise RuntimeError("at least one read access specification required") + + # build the request + request = ReadPropertyMultipleRequest( + listOfReadAccessSpecs=read_access_spec_list + ) + request.pduDestination = Address(addr) + if _debug: + DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: + DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyMultipleACK): + if _debug: + DiscoverConsoleCmd._debug(" - not an ack") + return + + # loop through the results + for result in apdu.listOfReadAccessResults: + # here is the object identifier + objectIdentifier = result.objectIdentifier + if _debug: + DiscoverConsoleCmd._debug( + " - objectIdentifier: %r", objectIdentifier + ) + + # now come the property values per object + for element in result.listOfResults: + # get the property and array index + propertyIdentifier = element.propertyIdentifier + if _debug: + DiscoverConsoleCmd._debug( + " - propertyIdentifier: %r", propertyIdentifier + ) + propertyArrayIndex = element.propertyArrayIndex + if _debug: + DiscoverConsoleCmd._debug( + " - propertyArrayIndex: %r", propertyArrayIndex + ) + + # here is the read result + readResult = element.readResult + + property_label = str(propertyIdentifier) + if propertyArrayIndex is not None: + property_label += "[" + str(propertyArrayIndex) + "]" + + # check for an error + if readResult.propertyAccessError is not None: + if interactive: + print( + "{} ! {}".format( + property_label, readResult.propertyAccessError + ) + ) + + else: + # here is the value + propertyValue = readResult.propertyValue + + # find the datatype + datatype = get_datatype( + objectIdentifier[0], propertyIdentifier + ) + if _debug: + DiscoverConsoleCmd._debug( + " - datatype: %r", datatype + ) + if not datatype: + str_value = "?" + else: + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and ( + propertyArrayIndex is not None + ): + if propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: + DiscoverConsoleCmd._debug( + " - datatype: %r", datatype + ) + + try: + value = propertyValue.cast_out(datatype) + if _debug: + DiscoverConsoleCmd._debug( + " - value: %r", value + ) + + # convert the value to a string + if hasattr(value, "dict_contents"): + dict_contents = value.dict_contents( + as_class=OrderedDict + ) + str_value = json.dumps(dict_contents) + else: + str_value = str(value) + except Exception as err: + str_value = "!" + str(err) + + if interactive: + print("{}: {}".format(property_label, str_value)) + + # save it in the snapshot + snapshot.upsert( + devid, + "{}:{}".format(*objectIdentifier), + str(propertyIdentifier), + str_value, + ) + + # do something for error/reject/abort + if iocb.ioError: + if interactive: + print(str(iocb.ioError)) + + except Exception as error: + DiscoverConsoleCmd._exception("exception: %r", error) + + +# +# __main__ +# + + +def main(): + global args, this_device, this_application, snapshot, who_is_to_do_list, application_to_do_list + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # input file for exiting configuration + parser.add_argument("infile", default="-", help="input file") + + # output file for discovered configuration + parser.add_argument( + "outfile", nargs="?", default="-unspecified-", help="output file" + ) + + args = parser.parse_args() + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = DiscoverApplication(this_device, args.ini.address) + if _debug: + _log.debug(" - this_application: %r", this_application) + + # make a snapshot 'database' + snapshot = Snapshot() + + # read in an existing snapshot + if args.infile != "-": + snapshot.read_file(args.infile) + + # special lists + # network_path_to_do_list = NetworkPathToDoList(this_application.nse) + who_is_to_do_list = WhoIsToDoList(this_application) + application_to_do_list = ApplicationToDoList() + + # make a console + this_console = DiscoverConsoleCmd() + _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + + # write out the snapshot, outfile defaults to infile if not specified + if args.outfile == "-unspecified-": + args.outfile = args.infile + if args.outfile != "-": + snapshot.write_file(args.outfile) + + +if __name__ == "__main__": + main() diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 249a4c06..9fa32d01 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -192,7 +192,10 @@ def do_read(self, args): datatype = Unsigned else: datatype = datatype.subtype - if _debug: ThreadedHTTPRequestHandler._debug(" - datatype: %r", datatype) + if _debug: + ThreadedHTTPRequestHandler._debug( + " - datatype: %r", datatype + ) # convert the value to a dict if possible value = apdu.propertyValue.cast_out(datatype) diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject1.py similarity index 52% rename from samples/LocalScheduleObject.py rename to samples/LocalScheduleObject1.py index 83776c85..04a6b137 100644 --- a/samples/LocalScheduleObject.py +++ b/samples/LocalScheduleObject1.py @@ -15,8 +15,15 @@ from bacpypes.primitivedata import Null, Integer, Real, Date, Time, CharacterString from bacpypes.constructeddata import ArrayOf, SequenceOf -from bacpypes.basetypes import CalendarEntry, DailySchedule, DateRange, \ - DeviceObjectPropertyReference, SpecialEvent, SpecialEventPeriod, TimeValue +from bacpypes.basetypes import ( + CalendarEntry, + DailySchedule, + DateRange, + DeviceObjectPropertyReference, + SpecialEvent, + SpecialEventPeriod, + TimeValue, +) from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -33,13 +40,14 @@ # TestConsoleCmd # + @bacpypes_debugging class TestConsoleCmd(ConsoleCmd): - def do_test(self, args): """test