diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 74c103c6..e99ac48b 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.4' +__version__ = '0.17.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index d45c212b..dcbd5ed8 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -2365,7 +2365,9 @@ class PriorityValue(Choice): , Element('datetime', DateTime, 1) ] -class PriorityArray(ArrayOf(PriorityValue)): +class PriorityArray(ArrayOf( + PriorityValue, fixed_length=16, prototype=PriorityValue(null=()), + )): pass class PropertyAccessResultAccessResult(Choice): diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index cdc096ec..6d04f93a 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -5,6 +5,7 @@ """ import sys +from copy import deepcopy as _deepcopy from .errors import DecodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag @@ -677,7 +678,7 @@ def dict_contents(self, use_dict=None, as_class=dict): # return this new type return _ListOf -@bacpypes_debugging(ListOf) +bacpypes_debugging(ListOf) # # Array @@ -696,15 +697,33 @@ class Array(object): _array_of_map = {} _array_of_classes = {} -def ArrayOf(klass): +def ArrayOf(klass, fixed_length=None, prototype=None): """Function to return a class that can encode and decode a list of some other type.""" global _array_of_map global _array_of_classes, _sequence_of_classes + # check the parameters for consistency + if issubclass(klass, Atomic): + if prototype is None: + pass + elif not klass.is_valid(prototype): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + else: + if prototype is None: + ### TODO This should be an error, a prototype should always be + ### required for non-atomic types, even if it's only klass() + ### for a default object which will be deep copied + pass + elif not isinstance(prototype, klass): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + + # build a signature of the parameters + array_signature = (klass, fixed_length, prototype) + # if this has already been built, return the cached one - if klass in _array_of_map: - return _array_of_map[klass] + if array_signature in _array_of_map: + return _array_of_map[array_signature] # no ArrayOf(ArrayOf(...)) allowed if klass in _array_of_classes: @@ -717,23 +736,60 @@ def ArrayOf(klass): class ArrayOf(Array): subtype = None + fixed_length = None + prototype = None def __init__(self, value=None): if value is None: self.value = [0] + if self.fixed_length is not None: + self.fix_length(self.fixed_length) + elif isinstance(value, list): + if (self.fixed_length is not None) and (len(value) != self.fixed_length): + raise ValueError("invalid array length") + self.value = [len(value)] self.value.extend(value) else: raise TypeError("invalid constructor datatype") + def fix_length(self, new_length): + if len(self.value) > new_length + 1: + # trim off the excess + del self.value[new_length + 1:] + elif len(self.value) < new_length + 1: + # how many do we need + element_count = new_length - len(self.value) + 1 + + # extend or append + if issubclass(self.subtype, Atomic): + if self.prototype is None: + extend_value = self.subtype().value + else: + extend_value = self.prototype + self.value.extend( [extend_value] * element_count ) + else: + for i in range(element_count): + if self.prototype is None: + append_value = self.subtype() + else: + append_value = _deepcopy(self.prototype) + self.value.append(append_value) + + self.value[0] = new_length + def append(self, value): + if self.fixed_length is not None: + raise TypeError("fixed length array") + if issubclass(self.subtype, Atomic): pass elif issubclass(self.subtype, AnyAtomic) and not isinstance(value, Atomic): raise TypeError("instance of an atomic type required") elif not isinstance(value, self.subtype): raise TypeError("%s value required" % (self.subtype.__name__,)) + self.value.append(value) self.value[0] = len(self.value) - 1 @@ -754,23 +810,19 @@ def __setitem__(self, item, value): # special length handling for index 0 if item == 0: - if value < self.value[0]: - # trim - self.value = self.value[0:value + 1] - elif value > self.value[0]: - # extend - if issubclass(self.subtype, Atomic): - self.value.extend( [self.subtype().value] * (value - self.value[0]) ) - else: - for i in range(value - self.value[0]): - self.value.append(self.subtype()) - else: + if (self.fixed_length is not None): + if (value != self.value[0]): + raise TypeError("fixed length array") return - self.value[0] = value + + self.fix_length(value) else: self.value[item] = value def __delitem__(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # no wrapping index if (item < 1) or (item > self.value[0]): raise IndexError("index out of range") @@ -792,6 +844,9 @@ def index(self, value): raise ValueError("%r not in array" % (value,)) def remove(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # find the index of the item and delete it indx = self.index(item) self.__delitem__(indx) @@ -820,7 +875,7 @@ def decode(self, taglist): if _debug: ArrayOf._debug("(%r)decode %r", self.__class__.__name__, taglist) # start with an empty array - self.value = [0] + new_value = [] while len(taglist) != 0: tag = taglist.Peek() @@ -835,7 +890,7 @@ def decode(self, taglist): helper = self.subtype(tag) # save the value - self.value.append(helper.value) + new_value.append(helper.value) else: if _debug: ArrayOf._debug(" - building value: %r", self.subtype) # build an element @@ -845,10 +900,15 @@ def decode(self, taglist): value.decode(taglist) # save what was built - self.value.append(value) + new_value.append(value) + + # check the length + if self.fixed_length is not None: + if self.fixed_length != len(new_value): + raise ValueError("invalid array length") # update the length - self.value[0] = len(self.value) - 1 + self.value = [len(new_value)] + new_value def encode_item(self, item, taglist): if _debug: ArrayOf._debug("(%r)encode_item %r %r", self.__class__.__name__, item, taglist) @@ -947,10 +1007,14 @@ def dict_contents(self, use_dict=None, as_class=dict): # constrain it to a list of a specific type of item setattr(ArrayOf, 'subtype', klass) + setattr(ArrayOf, 'fixed_length', fixed_length) + setattr(ArrayOf, 'prototype', prototype) + + # update the name ArrayOf.__name__ = 'ArrayOf' + klass.__name__ # cache this type - _array_of_map[klass] = ArrayOf + _array_of_map[array_signature] = ArrayOf _array_of_classes[ArrayOf] = 1 # return this new type @@ -1153,7 +1217,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" - if _debug: _log.debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + if _debug: Choice._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) # make/extend the dictionary of content if use_dict is None: @@ -1347,7 +1411,7 @@ def dict_contents(self, use_dict=None, as_class=dict): # AnyAtomic # -class AnyAtomic: +class AnyAtomic(Atomic): def __init__(self, arg=None): if _debug: AnyAtomic._debug("__init__ %r", arg) @@ -1396,3 +1460,4 @@ def __repr__(self): return '<' + desc + ' instance at 0x%08x' % (id(self),) + '>' bacpypes_debugging(AnyAtomic) + diff --git a/py25/bacpypes/netservice.py b/py25/bacpypes/netservice.py index 97278ff4..35aff03d 100755 --- a/py25/bacpypes/netservice.py +++ b/py25/bacpypes/netservice.py @@ -11,9 +11,11 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement +from .task import FunctionTask from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation -from .npdu import IAmRouterToNetwork, NPDU, WhoIsRouterToNetwork, npdu_types +from .npdu import NPDU, npdu_types, IAmRouterToNetwork, WhoIsRouterToNetwork, \ + WhatIsNetworkNumber, NetworkNumberIs from .apdu import APDU as _APDU # some debugging @@ -163,7 +165,7 @@ def delete_router_info(self, snet, address=None, dnets=None): class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet') + _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') def __init__(self, sap, net, cid=None): if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) @@ -171,6 +173,12 @@ def __init__(self, sap, net, cid=None): self.adapterSAP = sap self.adapterNet = net + # record if this was 0=learned, 1=configured, None=unknown + if net is None: + self.adapterNetConfigured = None + else: + self.adapterNetConfigured = 1 + def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -201,11 +209,11 @@ def DisconnectConnectionToNetwork(self, net): class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): - _debug_contents = ('adapters++', 'routers++', 'networks+' - , 'localAdapter-', 'localAddress' + _debug_contents = ('adapters++', 'pending_nets', + 'local_adapter-', 'local_address', ) - def __init__(self, routerInfoCache=None, sap=None, sid=None): + def __init__(self, router_info_cache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) @@ -214,7 +222,7 @@ def __init__(self, routerInfoCache=None, sap=None, sid=None): self.adapters = {} # net -> NetworkAdapter # use the provided cache or make a default one - self.router_info_cache = routerInfoCache or RouterInfoCache() + self.router_info_cache = router_info_cache or RouterInfoCache() # map to a list of application layer packets waiting for a path self.pending_nets = {} @@ -231,18 +239,13 @@ def bind(self, server, net=None, address=None): if net in self.adapters: raise RuntimeError("already bound") - # when binding to an adapter and there is more than one, then they - # must all have network numbers and one of them will be the default - if (net is not None) and (None in self.adapters): - raise RuntimeError("default adapter bound") - # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) self.adapters[net] = adapter if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one - if address: + if address and not self.local_address: self.local_adapter = adapter self.local_address = address @@ -332,9 +335,17 @@ def indication(self, pdu): # if the network matches the local adapter it's local if (dnet == adapter.adapterNet): - ### log this, the application shouldn't be sending to a remote station address - ### when it's a directly connected network - raise RuntimeError("addressing problem") + if (npdu.pduDestination.addrType == Address.remoteStationAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") + npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) + elif (npdu.pduDestination.addrType == Address.remoteBroadcastAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote broadcast to local broadcast") + npdu.pduDestination = LocalBroadcast() + else: + raise RuntimeError("addressing problem") + + adapter.process_npdu(npdu) + return # get it ready to send when the path is found npdu.pduDestination = None @@ -491,7 +502,7 @@ def process_npdu(self, adapter, npdu): elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address else: # combine the source address if npdu.npduSADR: @@ -673,6 +684,9 @@ def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) + # network number is timeout + self.network_number_is_task = None + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -689,6 +703,84 @@ def confirmation(self, adapter, npdu): if hasattr(self, fn): getattr(self, fn)(adapter, npdu) + def i_am_router_to_network(self, adapter=None, destination=None, network=None): + if _debug: NetworkServiceElement._debug("i_am_router_to_network %r %r %r", adapter, destination, network) + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # if we're not a router, trouble + if len(sap.adapters) == 1: + raise RuntimeError("not a router") + + if adapter is not None: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType in (Address.localStationAddr, Address.localBroadcastAddr): + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote station for a different adapter") + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote broadcast for a different adapter") + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + else: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType == Address.localStationAddr: + raise ValueError("ambiguous destination") + elif destination.addrType == Address.localBroadcastAddr: + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote station") + adapter = sap.adapters[destination.addrNet] + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote broadcast") + adapter = sap.adapters[destination.addrNet] + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + if _debug: NetworkServiceElement._debug(" - adapter, destination, network: %r, %r, %r", adapter, destination, network) + + # process a single adapter or all of the adapters + if adapter is not None: + adapter_list = [adapter] + else: + adapter_list = list(sap.adapters.values()) + + # loop through all of the adapters + for adapter in adapter_list: + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + netlist.append(xadapter.adapterNet) + ### add the other reachable networks + + if network is not None: + if network not in netlist: + continue + netlist = [network] + + # build a response + iamrtn = IAmRouterToNetwork(netlist) + iamrtn.pduDestination = destination + + if _debug: NetworkServiceElement._debug(" - adapter, iamrtn: %r, %r", adapter, iamrtn) + + # send it back + self.request(adapter, iamrtn) + #----- def WhoIsRouterToNetwork(self, adapter, npdu): @@ -889,5 +981,153 @@ def DisconnectConnectionToNetwork(self, adapter, npdu): # reference the service access point # sap = self.elementService + def what_is_network_number(self, adapter=None, address=None): + if _debug: NetworkServiceElement._debug("what_is_network_number %r", adapter, address) + + # reference the service access point + sap = self.elementService + + # a little error checking + if (adapter is None) and (address is not None): + raise RuntimeError("inconsistent parameters") + + # build a request + winn = WhatIsNetworkNumber() + winn.pduDestination = LocalBroadcast() + + # check for a specific adapter + if adapter: + if address is not None: + winn.pduDestination = address + adapter_list = [adapter] + else: + # send to adapters we don't know anything about + adapter_list = [] + for xadapter in sap.adapters.values(): + if xadapter.adapterNet is None: + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # send it to the adapter(s) + for xadapter in adapter_list: + self.request(xadapter, winn) + + def WhatIsNetworkNumber(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("WhatIsNetworkNumber %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known") + return + + # if this is not a router, wait for somebody else to answer + if (npdu.pduDestination.addrType == Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - local broadcast request") + + if len(sap.adapters) == 1: + if _debug: NetworkServiceElement._debug(" - not a router") + + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - already waiting") + else: + self.network_number_is_task = FunctionTask(self.network_number_is, adapter) + self.network_number_is_task.install_task(delta=10 * 1000) + return + + # send out what we know + self.network_number_is(adapter) + + def network_number_is(self, adapter=None): + if _debug: NetworkServiceElement._debug("network_number_is %r", adapter) + + # reference the service access point + sap = self.elementService + + # specific adapter, or all configured adapters + if adapter is not None: + adapter_list = [adapter] + else: + # send to adapters we are configured to know + adapter_list = [] + for xadapter in sap.adapters.values(): + if (xadapter.adapterNet is not None) and (xadapter.adapterNetConfigured == 1): + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # loop through the adapter(s) + for xadapter in adapter_list: + if xadapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - unknown network: %r", xadapter) + continue + + # build a broadcast annoucement + nni = NetworkNumberIs(net=xadapter.adapterNet, flag=xadapter.adapterNetConfigured) + nni.pduDestination = LocalBroadcast() + if _debug: NetworkServiceElement._debug(" - nni: %r", nni) + + # send it to the adapter + self.request(xadapter, nni) + + def NetworkNumberIs(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("NetworkNumberIs %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # if this was not sent as a broadcast, ignore it + if (npdu.pduDestination.addrType != Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - not broadcast") + return + + # if we are waiting for someone else to say what this network number + # is, cancel that task + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - cancel waiting task") + self.network_number_is_task.suspend_task() + self.network_number_is_task = None + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + + # delete the reference from an unknown network + del sap.adapters[None] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = 0 + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + if _debug: NetworkServiceElement._debug(" - local network learned") + ###TODO: s/None/net/g in routing tables + return + + # check if this matches what we have + if adapter.adapterNet == npdu.nniNet: + if _debug: NetworkServiceElement._debug(" - matches what we have") + return + + # check it this matches what we know, if we know it + if adapter.adapterNetConfigured == 1: + if _debug: NetworkServiceElement._debug(" - doesn't match what we know") + return + + if _debug: NetworkServiceElement._debug(" - learning something new") + + # delete the reference from the old (learned) network + del sap.adapters[adapter.adapterNet] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = npdu.nniFlag + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + ###TODO: s/old/new/g in routing tables + bacpypes_debugging(NetworkServiceElement) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index c5ce0cae..297d035c 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -928,7 +928,7 @@ class AlertEnrollmentObject(Object): properties = \ [ ReadableProperty('presentValue', ObjectIdentifier) , ReadableProperty('eventState', EventState) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , ReadableProperty('notificationClass', Unsigned) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 5367ed05..bf3bf12a 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -1662,6 +1662,16 @@ def __init__(self, *args): self.set_long(long(arg)) elif isinstance(arg, long): self.set_long(arg) + elif isinstance(arg, basestring): + try: + objType, objInstance = arg.split(':') + if objType.isdigit(): + objType = int(objType) + objInstance = int(objInstance) + except: + raise ValueError("invalid format") + + self.set_tuple(objType, objInstance) elif isinstance(arg, tuple): self.set_tuple(*arg) elif isinstance(arg, ObjectIdentifier): diff --git a/py25/bacpypes/service/cov.py b/py25/bacpypes/service/cov.py index 3e79c9cb..d37b7515 100644 --- a/py25/bacpypes/service/cov.py +++ b/py25/bacpypes/service/cov.py @@ -8,7 +8,7 @@ from ..capability import Capability from ..core import deferred -from ..task import OneShotTask, TaskManager +from ..task import OneShotTask, RecurringFunctionTask, TaskManager from ..iocb import IOCB from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ @@ -167,6 +167,23 @@ def __init__(self, obj): # list of all active subscriptions self.cov_subscriptions = SubscriptionList() + def add_subscription(self, cov): + if _debug: COVDetection._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: COVDetection._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: COVDetection._debug(" - task suspended") + + # remove it from the subscription list for its object + self.cov_subscriptions.remove(cov) + def execute(self): if _debug: COVDetection._debug("execute") @@ -297,20 +314,20 @@ def present_value_filter(self, old_value, new_value): self.previous_reported_value = old_value # see if it changed enough to trigger reporting - value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ - or (new_value >= (self.previous_reported_value + self.covIncrement)) + value_changed = (new_value <= (self.previous_reported_value - self.obj.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.obj.covIncrement)) if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) return value_changed - def send_cov_notifications(self): - if _debug: COVIncrementCriteria._debug("send_cov_notifications") + def send_cov_notifications(self, subscription=None): + if _debug: COVIncrementCriteria._debug("send_cov_notifications %r", subscription) # when sending out notifications, keep the current value self.previous_reported_value = self.presentValue # continue - COVDetection.send_cov_notifications(self) + COVDetection.send_cov_notifications(self, subscription) bacpypes_debugging(COVDetection) @@ -379,17 +396,82 @@ class LoadControlCriteria(COVDetection): ) -class PulseConverterCriteria(COVDetection): +class PulseConverterCriteria(COVIncrementCriteria): properties_tracked = ( 'presentValue', 'statusFlags', + 'covPeriod', ) properties_reported = ( 'presentValue', 'statusFlags', ) + def __init__(self, obj): + if _debug: PulseConverterCriteria._debug("__init__ %r", obj) + COVIncrementCriteria.__init__(self, obj) + + # check for a period + if self.covPeriod == 0: + if _debug: PulseConverterCriteria._debug(" - no periodic notifications") + self.cov_period_task = None + else: + if _debug: PulseConverterCriteria._debug(" - covPeriod: %r", self.covPeriod) + self.cov_period_task = RecurringFunctionTask(self.covPeriod * 1000, self.send_cov_notifications) + if _debug: PulseConverterCriteria._debug(" - cov period task created") + + def add_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("add_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.add_subscription(self, cov) + + # if there is a COV period task, install it + if self.cov_period_task: + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - cov period task installed") + + def cancel_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("cancel_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.cancel_subscription(self, cov) + + # if there are no more subscriptions, cancel the task + if not len(self.cov_subscriptions): + if self.cov_period_task and self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - cov period task suspended") + self.cov_period_task = None + + @monitor_filter('covPeriod') + def cov_period_filter(self, old_value, new_value): + if _debug: PulseConverterCriteria._debug("cov_period_filter %r %r", old_value, new_value) + + # check for an old period + if old_value != 0: + if self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - canceled old task") + self.cov_period_task = None + + # check for a new period + if new_value != 0: + self.cov_period_task = RecurringFunctionTask(new_value * 1000, self.cov_period_x) + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - new task created and installed") + + return False + + def send_cov_notifications(self, subscription=None): + if _debug: PulseConverterCriteria._debug("send_cov_notifications %r", subscription) + + # pass along to the parent class as if something changed + COVIncrementCriteria.send_cov_notifications(self, subscription) + +bacpypes_debugging(PulseConverterCriteria) + # mapping from object type to appropriate criteria class criteria_type_map = { @@ -516,22 +598,17 @@ def __init__(self): def add_subscription(self, cov): if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) - # add it to the subscription list for its object - self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + # let the detection algorithm know this is a new or additional subscription + self.cov_detections[cov.obj_ref].add_subscription(cov) def cancel_subscription(self, cov): if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) - # cancel the subscription timeout - if cov.isScheduled: - cov.suspend_task() - if _debug: ChangeOfValueServices._debug(" - task suspended") - # get the detection algorithm object cov_detection = self.cov_detections[cov.obj_ref] - # remove it from the subscription list for its object - cov_detection.cov_subscriptions.remove(cov) + # let the detection algorithm know this subscription is going away + cov_detection.cancel_subscription(cov) # if the detection algorithm doesn't have any subscriptions, remove it if not len(cov_detection.cov_subscriptions): diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 74c103c6..e99ac48b 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.4' +__version__ = '0.17.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index 1548a3b4..ad24d4fc 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -517,7 +517,7 @@ def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): bind(self.bip, self.annexj, self.mux.annexJ) # bind the BIP stack to the network, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) def close_socket(self): if _debug: BIPSimpleApplication._debug("close_socket") @@ -578,7 +578,7 @@ def __init__(self, localDevice, localAddress, bbmdAddress, bbmdTTL, deviceInfoCa bind(self.bip, self.annexj, self.mux.annexJ) # bind the NSAP to the stack, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) def close_socket(self): if _debug: BIPForeignApplication._debug("close_socket") @@ -619,4 +619,5 @@ def __init__(self, localAddress, eID=None): bind(self.bip, self.annexj, self.mux.annexJ) # bind the NSAP to the stack, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) + diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index d45c212b..dcbd5ed8 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -2365,7 +2365,9 @@ class PriorityValue(Choice): , Element('datetime', DateTime, 1) ] -class PriorityArray(ArrayOf(PriorityValue)): +class PriorityArray(ArrayOf( + PriorityValue, fixed_length=16, prototype=PriorityValue(null=()), + )): pass class PropertyAccessResultAccessResult(Choice): diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index bad87d0d..8dd27af4 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -5,6 +5,7 @@ """ import sys +from copy import deepcopy as _deepcopy from .errors import DecodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag @@ -691,15 +692,33 @@ class Array(object): _array_of_map = {} _array_of_classes = {} -def ArrayOf(klass): +def ArrayOf(klass, fixed_length=None, prototype=None): """Function to return a class that can encode and decode a list of some other type.""" global _array_of_map global _array_of_classes, _sequence_of_classes + # check the parameters for consistency + if issubclass(klass, Atomic): + if prototype is None: + pass + elif not klass.is_valid(prototype): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + else: + if prototype is None: + ### TODO This should be an error, a prototype should always be + ### required for non-atomic types, even if it's only klass() + ### for a default object which will be deep copied + pass + elif not isinstance(prototype, klass): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + + # build a signature of the parameters + array_signature = (klass, fixed_length, prototype) + # if this has already been built, return the cached one - if klass in _array_of_map: - return _array_of_map[klass] + if array_signature in _array_of_map: + return _array_of_map[array_signature] # no ArrayOf(ArrayOf(...)) allowed if klass in _array_of_classes: @@ -713,23 +732,60 @@ def ArrayOf(klass): class ArrayOf(Array): subtype = None + fixed_length = None + prototype = None def __init__(self, value=None): if value is None: self.value = [0] + if self.fixed_length is not None: + self.fix_length(self.fixed_length) + elif isinstance(value, list): + if (self.fixed_length is not None) and (len(value) != self.fixed_length): + raise ValueError("invalid array length") + self.value = [len(value)] self.value.extend(value) else: raise TypeError("invalid constructor datatype") + def fix_length(self, new_length): + if len(self.value) > new_length + 1: + # trim off the excess + del self.value[new_length + 1:] + elif len(self.value) < new_length + 1: + # how many do we need + element_count = new_length - len(self.value) + 1 + + # extend or append + if issubclass(self.subtype, Atomic): + if self.prototype is None: + extend_value = self.subtype().value + else: + extend_value = self.prototype + self.value.extend( [extend_value] * element_count ) + else: + for i in range(element_count): + if self.prototype is None: + append_value = self.subtype() + else: + append_value = _deepcopy(self.prototype) + self.value.append(append_value) + + self.value[0] = new_length + def append(self, value): + if self.fixed_length is not None: + raise TypeError("fixed length array") + if issubclass(self.subtype, Atomic): pass elif issubclass(self.subtype, AnyAtomic) and not isinstance(value, Atomic): raise TypeError("instance of an atomic type required") elif not isinstance(value, self.subtype): raise TypeError("%s value required" % (self.subtype.__name__,)) + self.value.append(value) self.value[0] = len(self.value) - 1 @@ -750,23 +806,19 @@ def __setitem__(self, item, value): # special length handling for index 0 if item == 0: - if value < self.value[0]: - # trim - self.value = self.value[0:value + 1] - elif value > self.value[0]: - # extend - if issubclass(self.subtype, Atomic): - self.value.extend( [self.subtype().value] * (value - self.value[0]) ) - else: - for i in range(value - self.value[0]): - self.value.append(self.subtype()) - else: + if (self.fixed_length is not None): + if (value != self.value[0]): + raise TypeError("fixed length array") return - self.value[0] = value + + self.fix_length(value) else: self.value[item] = value def __delitem__(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # no wrapping index if (item < 1) or (item > self.value[0]): raise IndexError("index out of range") @@ -788,6 +840,9 @@ def index(self, value): raise ValueError("%r not in array" % (value,)) def remove(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # find the index of the item and delete it indx = self.index(item) self.__delitem__(indx) @@ -816,7 +871,7 @@ def decode(self, taglist): if _debug: ArrayOf._debug("(%r)decode %r", self.__class__.__name__, taglist) # start with an empty array - self.value = [0] + new_value = [] while len(taglist) != 0: tag = taglist.Peek() @@ -831,7 +886,7 @@ def decode(self, taglist): helper = self.subtype(tag) # save the value - self.value.append(helper.value) + new_value.append(helper.value) else: if _debug: ArrayOf._debug(" - building value: %r", self.subtype) # build an element @@ -841,10 +896,15 @@ def decode(self, taglist): value.decode(taglist) # save what was built - self.value.append(value) + new_value.append(value) + + # check the length + if self.fixed_length is not None: + if self.fixed_length != len(new_value): + raise ValueError("invalid array length") # update the length - self.value[0] = len(self.value) - 1 + self.value = [len(new_value)] + new_value def encode_item(self, item, taglist): if _debug: ArrayOf._debug("(%r)encode_item %r %r", self.__class__.__name__, item, taglist) @@ -941,10 +1001,14 @@ def dict_contents(self, use_dict=None, as_class=dict): # constrain it to a list of a specific type of item setattr(ArrayOf, 'subtype', klass) + setattr(ArrayOf, 'fixed_length', fixed_length) + setattr(ArrayOf, 'prototype', prototype) + + # update the name ArrayOf.__name__ = 'ArrayOf' + klass.__name__ # cache this type - _array_of_map[klass] = ArrayOf + _array_of_map[array_signature] = ArrayOf _array_of_classes[ArrayOf] = 1 # return this new type @@ -1148,7 +1212,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" - if _debug: _log.debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + if _debug: Choice._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) # make/extend the dictionary of content if use_dict is None: diff --git a/py27/bacpypes/netservice.py b/py27/bacpypes/netservice.py index 64c5bfb4..bfb44a77 100755 --- a/py27/bacpypes/netservice.py +++ b/py27/bacpypes/netservice.py @@ -11,9 +11,11 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement +from .task import FunctionTask from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation -from .npdu import IAmRouterToNetwork, NPDU, WhoIsRouterToNetwork, npdu_types +from .npdu import NPDU, npdu_types, IAmRouterToNetwork, WhoIsRouterToNetwork, \ + WhatIsNetworkNumber, NetworkNumberIs from .apdu import APDU as _APDU # some debugging @@ -163,7 +165,7 @@ def delete_router_info(self, snet, address=None, dnets=None): @bacpypes_debugging class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet') + _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') def __init__(self, sap, net, cid=None): if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) @@ -171,6 +173,12 @@ def __init__(self, sap, net, cid=None): self.adapterSAP = sap self.adapterNet = net + # record if this was 0=learned, 1=configured, None=unknown + if net is None: + self.adapterNetConfigured = None + else: + self.adapterNetConfigured = 1 + def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -200,11 +208,11 @@ def DisconnectConnectionToNetwork(self, net): @bacpypes_debugging class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): - _debug_contents = ('adapters++', 'routers++', 'networks+' - , 'localAdapter-', 'localAddress' + _debug_contents = ('adapters++', 'pending_nets', + 'local_adapter-', 'local_address', ) - def __init__(self, routerInfoCache=None, sap=None, sid=None): + def __init__(self, router_info_cache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) @@ -213,7 +221,7 @@ def __init__(self, routerInfoCache=None, sap=None, sid=None): self.adapters = {} # net -> NetworkAdapter # use the provided cache or make a default one - self.router_info_cache = routerInfoCache or RouterInfoCache() + self.router_info_cache = router_info_cache or RouterInfoCache() # map to a list of application layer packets waiting for a path self.pending_nets = {} @@ -230,18 +238,13 @@ def bind(self, server, net=None, address=None): if net in self.adapters: raise RuntimeError("already bound") - # when binding to an adapter and there is more than one, then they - # must all have network numbers and one of them will be the default - if (net is not None) and (None in self.adapters): - raise RuntimeError("default adapter bound") - # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) self.adapters[net] = adapter if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one - if address: + if address and not self.local_address: self.local_adapter = adapter self.local_address = address @@ -331,9 +334,17 @@ def indication(self, pdu): # if the network matches the local adapter it's local if (dnet == adapter.adapterNet): - ### log this, the application shouldn't be sending to a remote station address - ### when it's a directly connected network - raise RuntimeError("addressing problem") + if (npdu.pduDestination.addrType == Address.remoteStationAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") + npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) + elif (npdu.pduDestination.addrType == Address.remoteBroadcastAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote broadcast to local broadcast") + npdu.pduDestination = LocalBroadcast() + else: + raise RuntimeError("addressing problem") + + adapter.process_npdu(npdu) + return # get it ready to send when the path is found npdu.pduDestination = None @@ -490,7 +501,7 @@ def process_npdu(self, adapter, npdu): elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address else: # combine the source address if npdu.npduSADR: @@ -671,6 +682,9 @@ def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) + # network number is timeout + self.network_number_is_task = None + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -687,6 +701,84 @@ def confirmation(self, adapter, npdu): if hasattr(self, fn): getattr(self, fn)(adapter, npdu) + def i_am_router_to_network(self, adapter=None, destination=None, network=None): + if _debug: NetworkServiceElement._debug("i_am_router_to_network %r %r %r", adapter, destination, network) + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # if we're not a router, trouble + if len(sap.adapters) == 1: + raise RuntimeError("not a router") + + if adapter is not None: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType in (Address.localStationAddr, Address.localBroadcastAddr): + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote station for a different adapter") + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote broadcast for a different adapter") + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + else: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType == Address.localStationAddr: + raise ValueError("ambiguous destination") + elif destination.addrType == Address.localBroadcastAddr: + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote station") + adapter = sap.adapters[destination.addrNet] + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote broadcast") + adapter = sap.adapters[destination.addrNet] + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + if _debug: NetworkServiceElement._debug(" - adapter, destination, network: %r, %r, %r", adapter, destination, network) + + # process a single adapter or all of the adapters + if adapter is not None: + adapter_list = [adapter] + else: + adapter_list = list(sap.adapters.values()) + + # loop through all of the adapters + for adapter in adapter_list: + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + netlist.append(xadapter.adapterNet) + ### add the other reachable networks + + if network is not None: + if network not in netlist: + continue + netlist = [network] + + # build a response + iamrtn = IAmRouterToNetwork(netlist) + iamrtn.pduDestination = destination + + if _debug: NetworkServiceElement._debug(" - adapter, iamrtn: %r, %r", adapter, iamrtn) + + # send it back + self.request(adapter, iamrtn) + #----- def WhoIsRouterToNetwork(self, adapter, npdu): @@ -887,3 +979,151 @@ def DisconnectConnectionToNetwork(self, adapter, npdu): # reference the service access point # sap = self.elementService + def what_is_network_number(self, adapter=None, address=None): + if _debug: NetworkServiceElement._debug("what_is_network_number %r", adapter, address) + + # reference the service access point + sap = self.elementService + + # a little error checking + if (adapter is None) and (address is not None): + raise RuntimeError("inconsistent parameters") + + # build a request + winn = WhatIsNetworkNumber() + winn.pduDestination = LocalBroadcast() + + # check for a specific adapter + if adapter: + if address is not None: + winn.pduDestination = address + adapter_list = [adapter] + else: + # send to adapters we don't know anything about + adapter_list = [] + for xadapter in sap.adapters.values(): + if xadapter.adapterNet is None: + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # send it to the adapter(s) + for xadapter in adapter_list: + self.request(xadapter, winn) + + def WhatIsNetworkNumber(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("WhatIsNetworkNumber %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known") + return + + # if this is not a router, wait for somebody else to answer + if (npdu.pduDestination.addrType == Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - local broadcast request") + + if len(sap.adapters) == 1: + if _debug: NetworkServiceElement._debug(" - not a router") + + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - already waiting") + else: + self.network_number_is_task = FunctionTask(self.network_number_is, adapter) + self.network_number_is_task.install_task(delta=10 * 1000) + return + + # send out what we know + self.network_number_is(adapter) + + def network_number_is(self, adapter=None): + if _debug: NetworkServiceElement._debug("network_number_is %r", adapter) + + # reference the service access point + sap = self.elementService + + # specific adapter, or all configured adapters + if adapter is not None: + adapter_list = [adapter] + else: + # send to adapters we are configured to know + adapter_list = [] + for xadapter in sap.adapters.values(): + if (xadapter.adapterNet is not None) and (xadapter.adapterNetConfigured == 1): + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # loop through the adapter(s) + for xadapter in adapter_list: + if xadapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - unknown network: %r", xadapter) + continue + + # build a broadcast annoucement + nni = NetworkNumberIs(net=xadapter.adapterNet, flag=xadapter.adapterNetConfigured) + nni.pduDestination = LocalBroadcast() + if _debug: NetworkServiceElement._debug(" - nni: %r", nni) + + # send it to the adapter + self.request(xadapter, nni) + + def NetworkNumberIs(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("NetworkNumberIs %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # if this was not sent as a broadcast, ignore it + if (npdu.pduDestination.addrType != Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - not broadcast") + return + + # if we are waiting for someone else to say what this network number + # is, cancel that task + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - cancel waiting task") + self.network_number_is_task.suspend_task() + self.network_number_is_task = None + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + + # delete the reference from an unknown network + del sap.adapters[None] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = 0 + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + if _debug: NetworkServiceElement._debug(" - local network learned") + ###TODO: s/None/net/g in routing tables + return + + # check if this matches what we have + if adapter.adapterNet == npdu.nniNet: + if _debug: NetworkServiceElement._debug(" - matches what we have") + return + + # check it this matches what we know, if we know it + if adapter.adapterNetConfigured == 1: + if _debug: NetworkServiceElement._debug(" - doesn't match what we know") + return + + if _debug: NetworkServiceElement._debug(" - learning something new") + + # delete the reference from the old (learned) network + del sap.adapters[adapter.adapterNet] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = npdu.nniFlag + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + ###TODO: s/old/new/g in routing tables + diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index ac39cc05..8bbff2e0 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -329,6 +329,8 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False arry[arrayIndex] = value except IndexError: raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + except TypeError: + raise ExecutionError(errorClass='property', errorCode='valueOutOfRange') # check for monitors, call each one with the old and new value if is_monitored: @@ -929,7 +931,7 @@ class AlertEnrollmentObject(Object): properties = \ [ ReadableProperty('presentValue', ObjectIdentifier) , ReadableProperty('eventState', EventState) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , ReadableProperty('notificationClass', Unsigned) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index ccc0d688..8d00be62 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -1668,6 +1668,16 @@ def __init__(self, *args): self.set_long(long(arg)) elif isinstance(arg, long): self.set_long(arg) + elif isinstance(arg, basestring): + try: + objType, objInstance = arg.split(':') + if objType.isdigit(): + objType = int(objType) + objInstance = int(objInstance) + except: + raise ValueError("invalid format") + + self.set_tuple(objType, objInstance) elif isinstance(arg, tuple): self.set_tuple(*arg) elif isinstance(arg, ObjectIdentifier): diff --git a/py27/bacpypes/service/cov.py b/py27/bacpypes/service/cov.py index 0bb07d5a..21fd0b60 100644 --- a/py27/bacpypes/service/cov.py +++ b/py27/bacpypes/service/cov.py @@ -8,7 +8,7 @@ from ..capability import Capability from ..core import deferred -from ..task import OneShotTask, TaskManager +from ..task import OneShotTask, RecurringFunctionTask, TaskManager from ..iocb import IOCB from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ @@ -167,6 +167,23 @@ def __init__(self, obj): # list of all active subscriptions self.cov_subscriptions = SubscriptionList() + def add_subscription(self, cov): + if _debug: COVDetection._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: COVDetection._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: COVDetection._debug(" - task suspended") + + # remove it from the subscription list for its object + self.cov_subscriptions.remove(cov) + def execute(self): if _debug: COVDetection._debug("execute") @@ -296,20 +313,20 @@ def present_value_filter(self, old_value, new_value): self.previous_reported_value = old_value # see if it changed enough to trigger reporting - value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ - or (new_value >= (self.previous_reported_value + self.covIncrement)) + value_changed = (new_value <= (self.previous_reported_value - self.obj.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.obj.covIncrement)) if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) return value_changed - def send_cov_notifications(self): - if _debug: COVIncrementCriteria._debug("send_cov_notifications") + def send_cov_notifications(self, subscription=None): + if _debug: COVIncrementCriteria._debug("send_cov_notifications %r", subscription) # when sending out notifications, keep the current value self.previous_reported_value = self.presentValue # continue - COVDetection.send_cov_notifications(self) + COVDetection.send_cov_notifications(self, subscription) class AccessDoorCriteria(COVDetection): @@ -376,17 +393,81 @@ class LoadControlCriteria(COVDetection): ) -class PulseConverterCriteria(COVDetection): +@bacpypes_debugging +class PulseConverterCriteria(COVIncrementCriteria): properties_tracked = ( 'presentValue', 'statusFlags', + 'covPeriod', ) properties_reported = ( 'presentValue', 'statusFlags', ) + def __init__(self, obj): + if _debug: PulseConverterCriteria._debug("__init__ %r", obj) + COVIncrementCriteria.__init__(self, obj) + + # check for a period + if self.covPeriod == 0: + if _debug: PulseConverterCriteria._debug(" - no periodic notifications") + self.cov_period_task = None + else: + if _debug: PulseConverterCriteria._debug(" - covPeriod: %r", self.covPeriod) + self.cov_period_task = RecurringFunctionTask(self.covPeriod * 1000, self.send_cov_notifications) + if _debug: PulseConverterCriteria._debug(" - cov period task created") + + def add_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("add_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.add_subscription(self, cov) + + # if there is a COV period task, install it + if self.cov_period_task: + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - cov period task installed") + + def cancel_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("cancel_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.cancel_subscription(self, cov) + + # if there are no more subscriptions, cancel the task + if not len(self.cov_subscriptions): + if self.cov_period_task and self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - cov period task suspended") + self.cov_period_task = None + + @monitor_filter('covPeriod') + def cov_period_filter(self, old_value, new_value): + if _debug: PulseConverterCriteria._debug("cov_period_filter %r %r", old_value, new_value) + + # check for an old period + if old_value != 0: + if self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - canceled old task") + self.cov_period_task = None + + # check for a new period + if new_value != 0: + self.cov_period_task = RecurringFunctionTask(new_value * 1000, self.cov_period_x) + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - new task created and installed") + + return False + + def send_cov_notifications(self, subscription=None): + if _debug: PulseConverterCriteria._debug("send_cov_notifications %r", subscription) + + # pass along to the parent class as if something changed + COVIncrementCriteria.send_cov_notifications(self, subscription) + # mapping from object type to appropriate criteria class criteria_type_map = { @@ -513,22 +594,17 @@ def __init__(self): def add_subscription(self, cov): if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) - # add it to the subscription list for its object - self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + # let the detection algorithm know this is a new or additional subscription + self.cov_detections[cov.obj_ref].add_subscription(cov) def cancel_subscription(self, cov): if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) - # cancel the subscription timeout - if cov.isScheduled: - cov.suspend_task() - if _debug: ChangeOfValueServices._debug(" - task suspended") - # get the detection algorithm object cov_detection = self.cov_detections[cov.obj_ref] - # remove it from the subscription list for its object - cov_detection.cov_subscriptions.remove(cov) + # let the detection algorithm know this subscription is going away + cov_detection.cancel_subscription(cov) # if the detection algorithm doesn't have any subscriptions, remove it if not len(cov_detection.cov_subscriptions): diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index b8b48247..10774264 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.4' +__version__ = '0.17.5' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index d45c212b..dcbd5ed8 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -2365,7 +2365,9 @@ class PriorityValue(Choice): , Element('datetime', DateTime, 1) ] -class PriorityArray(ArrayOf(PriorityValue)): +class PriorityArray(ArrayOf( + PriorityValue, fixed_length=16, prototype=PriorityValue(null=()), + )): pass class PropertyAccessResultAccessResult(Choice): diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 574901ae..8dd27af4 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -5,6 +5,7 @@ """ import sys +from copy import deepcopy as _deepcopy from .errors import DecodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag @@ -691,15 +692,33 @@ class Array(object): _array_of_map = {} _array_of_classes = {} -def ArrayOf(klass): +def ArrayOf(klass, fixed_length=None, prototype=None): """Function to return a class that can encode and decode a list of some other type.""" global _array_of_map global _array_of_classes, _sequence_of_classes + # check the parameters for consistency + if issubclass(klass, Atomic): + if prototype is None: + pass + elif not klass.is_valid(prototype): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + else: + if prototype is None: + ### TODO This should be an error, a prototype should always be + ### required for non-atomic types, even if it's only klass() + ### for a default object which will be deep copied + pass + elif not isinstance(prototype, klass): + raise ValueError("prototype %r not valid for %s" % (prototype, klass.__name__)) + + # build a signature of the parameters + array_signature = (klass, fixed_length, prototype) + # if this has already been built, return the cached one - if klass in _array_of_map: - return _array_of_map[klass] + if array_signature in _array_of_map: + return _array_of_map[array_signature] # no ArrayOf(ArrayOf(...)) allowed if klass in _array_of_classes: @@ -713,23 +732,60 @@ def ArrayOf(klass): class ArrayOf(Array): subtype = None + fixed_length = None + prototype = None def __init__(self, value=None): if value is None: self.value = [0] + if self.fixed_length is not None: + self.fix_length(self.fixed_length) + elif isinstance(value, list): + if (self.fixed_length is not None) and (len(value) != self.fixed_length): + raise ValueError("invalid array length") + self.value = [len(value)] self.value.extend(value) else: raise TypeError("invalid constructor datatype") + def fix_length(self, new_length): + if len(self.value) > new_length + 1: + # trim off the excess + del self.value[new_length + 1:] + elif len(self.value) < new_length + 1: + # how many do we need + element_count = new_length - len(self.value) + 1 + + # extend or append + if issubclass(self.subtype, Atomic): + if self.prototype is None: + extend_value = self.subtype().value + else: + extend_value = self.prototype + self.value.extend( [extend_value] * element_count ) + else: + for i in range(element_count): + if self.prototype is None: + append_value = self.subtype() + else: + append_value = _deepcopy(self.prototype) + self.value.append(append_value) + + self.value[0] = new_length + def append(self, value): + if self.fixed_length is not None: + raise TypeError("fixed length array") + if issubclass(self.subtype, Atomic): pass elif issubclass(self.subtype, AnyAtomic) and not isinstance(value, Atomic): raise TypeError("instance of an atomic type required") elif not isinstance(value, self.subtype): raise TypeError("%s value required" % (self.subtype.__name__,)) + self.value.append(value) self.value[0] = len(self.value) - 1 @@ -750,23 +806,19 @@ def __setitem__(self, item, value): # special length handling for index 0 if item == 0: - if value < self.value[0]: - # trim - self.value = self.value[0:value + 1] - elif value > self.value[0]: - # extend - if issubclass(self.subtype, Atomic): - self.value.extend( [self.subtype().value] * (value - self.value[0]) ) - else: - for i in range(value - self.value[0]): - self.value.append(self.subtype()) - else: + if (self.fixed_length is not None): + if (value != self.value[0]): + raise TypeError("fixed length array") return - self.value[0] = value + + self.fix_length(value) else: self.value[item] = value def __delitem__(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # no wrapping index if (item < 1) or (item > self.value[0]): raise IndexError("index out of range") @@ -788,6 +840,9 @@ def index(self, value): raise ValueError("%r not in array" % (value,)) def remove(self, item): + if self.fixed_length is not None: + raise TypeError("fixed length array") + # find the index of the item and delete it indx = self.index(item) self.__delitem__(indx) @@ -816,7 +871,7 @@ def decode(self, taglist): if _debug: ArrayOf._debug("(%r)decode %r", self.__class__.__name__, taglist) # start with an empty array - self.value = [0] + new_value = [] while len(taglist) != 0: tag = taglist.Peek() @@ -831,7 +886,7 @@ def decode(self, taglist): helper = self.subtype(tag) # save the value - self.value.append(helper.value) + new_value.append(helper.value) else: if _debug: ArrayOf._debug(" - building value: %r", self.subtype) # build an element @@ -841,10 +896,15 @@ def decode(self, taglist): value.decode(taglist) # save what was built - self.value.append(value) + new_value.append(value) + + # check the length + if self.fixed_length is not None: + if self.fixed_length != len(new_value): + raise ValueError("invalid array length") # update the length - self.value[0] = len(self.value) - 1 + self.value = [len(new_value)] + new_value def encode_item(self, item, taglist): if _debug: ArrayOf._debug("(%r)encode_item %r %r", self.__class__.__name__, item, taglist) @@ -941,10 +1001,14 @@ def dict_contents(self, use_dict=None, as_class=dict): # constrain it to a list of a specific type of item setattr(ArrayOf, 'subtype', klass) + setattr(ArrayOf, 'fixed_length', fixed_length) + setattr(ArrayOf, 'prototype', prototype) + + # update the name ArrayOf.__name__ = 'ArrayOf' + klass.__name__ # cache this type - _array_of_map[klass] = ArrayOf + _array_of_map[array_signature] = ArrayOf _array_of_classes[ArrayOf] = 1 # return this new type @@ -1148,7 +1212,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" - if _debug: _log.debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + if _debug: Choice._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) # make/extend the dictionary of content if use_dict is None: @@ -1340,7 +1404,7 @@ def dict_contents(self, use_dict=None, as_class=dict): # @bacpypes_debugging -class AnyAtomic: +class AnyAtomic(Atomic): def __init__(self, arg=None): if _debug: AnyAtomic._debug("__init__ %r", arg) diff --git a/py34/bacpypes/netservice.py b/py34/bacpypes/netservice.py index 64c5bfb4..bfb44a77 100755 --- a/py34/bacpypes/netservice.py +++ b/py34/bacpypes/netservice.py @@ -11,9 +11,11 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement +from .task import FunctionTask from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation -from .npdu import IAmRouterToNetwork, NPDU, WhoIsRouterToNetwork, npdu_types +from .npdu import NPDU, npdu_types, IAmRouterToNetwork, WhoIsRouterToNetwork, \ + WhatIsNetworkNumber, NetworkNumberIs from .apdu import APDU as _APDU # some debugging @@ -163,7 +165,7 @@ def delete_router_info(self, snet, address=None, dnets=None): @bacpypes_debugging class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet') + _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') def __init__(self, sap, net, cid=None): if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) @@ -171,6 +173,12 @@ def __init__(self, sap, net, cid=None): self.adapterSAP = sap self.adapterNet = net + # record if this was 0=learned, 1=configured, None=unknown + if net is None: + self.adapterNetConfigured = None + else: + self.adapterNetConfigured = 1 + def confirmation(self, pdu): """Decode upstream PDUs and pass them up to the service access point.""" if _debug: NetworkAdapter._debug("confirmation %r (net=%r)", pdu, self.adapterNet) @@ -200,11 +208,11 @@ def DisconnectConnectionToNetwork(self, net): @bacpypes_debugging class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): - _debug_contents = ('adapters++', 'routers++', 'networks+' - , 'localAdapter-', 'localAddress' + _debug_contents = ('adapters++', 'pending_nets', + 'local_adapter-', 'local_address', ) - def __init__(self, routerInfoCache=None, sap=None, sid=None): + def __init__(self, router_info_cache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) @@ -213,7 +221,7 @@ def __init__(self, routerInfoCache=None, sap=None, sid=None): self.adapters = {} # net -> NetworkAdapter # use the provided cache or make a default one - self.router_info_cache = routerInfoCache or RouterInfoCache() + self.router_info_cache = router_info_cache or RouterInfoCache() # map to a list of application layer packets waiting for a path self.pending_nets = {} @@ -230,18 +238,13 @@ def bind(self, server, net=None, address=None): if net in self.adapters: raise RuntimeError("already bound") - # when binding to an adapter and there is more than one, then they - # must all have network numbers and one of them will be the default - if (net is not None) and (None in self.adapters): - raise RuntimeError("default adapter bound") - # create an adapter object, add it to our map adapter = NetworkAdapter(self, net) self.adapters[net] = adapter if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) # if the address was given, make it the "local" one - if address: + if address and not self.local_address: self.local_adapter = adapter self.local_address = address @@ -331,9 +334,17 @@ def indication(self, pdu): # if the network matches the local adapter it's local if (dnet == adapter.adapterNet): - ### log this, the application shouldn't be sending to a remote station address - ### when it's a directly connected network - raise RuntimeError("addressing problem") + if (npdu.pduDestination.addrType == Address.remoteStationAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") + npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) + elif (npdu.pduDestination.addrType == Address.remoteBroadcastAddr): + if _debug: NetworkServiceAccessPoint._debug(" - mapping remote broadcast to local broadcast") + npdu.pduDestination = LocalBroadcast() + else: + raise RuntimeError("addressing problem") + + adapter.process_npdu(npdu) + return # get it ready to send when the path is found npdu.pduDestination = None @@ -490,7 +501,7 @@ def process_npdu(self, adapter, npdu): elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address else: # combine the source address if npdu.npduSADR: @@ -671,6 +682,9 @@ def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) + # network number is timeout + self.network_number_is_task = None + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -687,6 +701,84 @@ def confirmation(self, adapter, npdu): if hasattr(self, fn): getattr(self, fn)(adapter, npdu) + def i_am_router_to_network(self, adapter=None, destination=None, network=None): + if _debug: NetworkServiceElement._debug("i_am_router_to_network %r %r %r", adapter, destination, network) + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # if we're not a router, trouble + if len(sap.adapters) == 1: + raise RuntimeError("not a router") + + if adapter is not None: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType in (Address.localStationAddr, Address.localBroadcastAddr): + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote station for a different adapter") + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet != adapter.adapterNet: + raise ValueError("invalid address, remote broadcast for a different adapter") + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + else: + if destination is None: + destination = LocalBroadcast() + elif destination.addrType == Address.localStationAddr: + raise ValueError("ambiguous destination") + elif destination.addrType == Address.localBroadcastAddr: + pass + elif destination.addrType == Address.remoteStationAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote station") + adapter = sap.adapters[destination.addrNet] + destination = LocalStation(destination.addrAddr) + elif destination.addrType == Address.remoteBroadcastAddr: + if destination.addrNet not in sap.adapters: + raise ValueError("invalid address, no network for remote broadcast") + adapter = sap.adapters[destination.addrNet] + destination = LocalBroadcast() + else: + raise TypeError("invalid destination address") + if _debug: NetworkServiceElement._debug(" - adapter, destination, network: %r, %r, %r", adapter, destination, network) + + # process a single adapter or all of the adapters + if adapter is not None: + adapter_list = [adapter] + else: + adapter_list = list(sap.adapters.values()) + + # loop through all of the adapters + for adapter in adapter_list: + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + netlist.append(xadapter.adapterNet) + ### add the other reachable networks + + if network is not None: + if network not in netlist: + continue + netlist = [network] + + # build a response + iamrtn = IAmRouterToNetwork(netlist) + iamrtn.pduDestination = destination + + if _debug: NetworkServiceElement._debug(" - adapter, iamrtn: %r, %r", adapter, iamrtn) + + # send it back + self.request(adapter, iamrtn) + #----- def WhoIsRouterToNetwork(self, adapter, npdu): @@ -887,3 +979,151 @@ def DisconnectConnectionToNetwork(self, adapter, npdu): # reference the service access point # sap = self.elementService + def what_is_network_number(self, adapter=None, address=None): + if _debug: NetworkServiceElement._debug("what_is_network_number %r", adapter, address) + + # reference the service access point + sap = self.elementService + + # a little error checking + if (adapter is None) and (address is not None): + raise RuntimeError("inconsistent parameters") + + # build a request + winn = WhatIsNetworkNumber() + winn.pduDestination = LocalBroadcast() + + # check for a specific adapter + if adapter: + if address is not None: + winn.pduDestination = address + adapter_list = [adapter] + else: + # send to adapters we don't know anything about + adapter_list = [] + for xadapter in sap.adapters.values(): + if xadapter.adapterNet is None: + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # send it to the adapter(s) + for xadapter in adapter_list: + self.request(xadapter, winn) + + def WhatIsNetworkNumber(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("WhatIsNetworkNumber %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known") + return + + # if this is not a router, wait for somebody else to answer + if (npdu.pduDestination.addrType == Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - local broadcast request") + + if len(sap.adapters) == 1: + if _debug: NetworkServiceElement._debug(" - not a router") + + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - already waiting") + else: + self.network_number_is_task = FunctionTask(self.network_number_is, adapter) + self.network_number_is_task.install_task(delta=10 * 1000) + return + + # send out what we know + self.network_number_is(adapter) + + def network_number_is(self, adapter=None): + if _debug: NetworkServiceElement._debug("network_number_is %r", adapter) + + # reference the service access point + sap = self.elementService + + # specific adapter, or all configured adapters + if adapter is not None: + adapter_list = [adapter] + else: + # send to adapters we are configured to know + adapter_list = [] + for xadapter in sap.adapters.values(): + if (xadapter.adapterNet is not None) and (xadapter.adapterNetConfigured == 1): + adapter_list.append(xadapter) + if _debug: NetworkServiceElement._debug(" - adapter_list: %r", adapter_list) + + # loop through the adapter(s) + for xadapter in adapter_list: + if xadapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - unknown network: %r", xadapter) + continue + + # build a broadcast annoucement + nni = NetworkNumberIs(net=xadapter.adapterNet, flag=xadapter.adapterNetConfigured) + nni.pduDestination = LocalBroadcast() + if _debug: NetworkServiceElement._debug(" - nni: %r", nni) + + # send it to the adapter + self.request(xadapter, nni) + + def NetworkNumberIs(self, adapter, npdu): + if _debug: NetworkServiceElement._debug("NetworkNumberIs %r %r", adapter, npdu) + + # reference the service access point + sap = self.elementService + + # if this was not sent as a broadcast, ignore it + if (npdu.pduDestination.addrType != Address.localBroadcastAddr): + if _debug: NetworkServiceElement._debug(" - not broadcast") + return + + # if we are waiting for someone else to say what this network number + # is, cancel that task + if self.network_number_is_task: + if _debug: NetworkServiceElement._debug(" - cancel waiting task") + self.network_number_is_task.suspend_task() + self.network_number_is_task = None + + # check to see if the local network is known + if adapter.adapterNet is None: + if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + + # delete the reference from an unknown network + del sap.adapters[None] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = 0 + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + if _debug: NetworkServiceElement._debug(" - local network learned") + ###TODO: s/None/net/g in routing tables + return + + # check if this matches what we have + if adapter.adapterNet == npdu.nniNet: + if _debug: NetworkServiceElement._debug(" - matches what we have") + return + + # check it this matches what we know, if we know it + if adapter.adapterNetConfigured == 1: + if _debug: NetworkServiceElement._debug(" - doesn't match what we know") + return + + if _debug: NetworkServiceElement._debug(" - learning something new") + + # delete the reference from the old (learned) network + del sap.adapters[adapter.adapterNet] + + adapter.adapterNet = npdu.nniNet + adapter.adapterNetConfigured = npdu.nniFlag + + # we now know what network this is + sap.adapters[adapter.adapterNet] = adapter + + ###TODO: s/old/new/g in routing tables + diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 9f2a4e99..8308abe8 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -929,7 +929,7 @@ class AlertEnrollmentObject(Object): properties = \ [ ReadableProperty('presentValue', ObjectIdentifier) , ReadableProperty('eventState', EventState) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , ReadableProperty('notificationClass', Unsigned) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index 3e89abe1..bb46f082 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -1652,6 +1652,16 @@ def __init__(self, *args): self.decode(arg) elif isinstance(arg, int): self.set_long(arg) + elif isinstance(arg, str): + try: + objType, objInstance = arg.split(':') + if objType.isdigit(): + objType = int(objType) + objInstance = int(objInstance) + except: + raise ValueError("invalid format") + + self.set_tuple(objType, objInstance) elif isinstance(arg, tuple): self.set_tuple(*arg) elif isinstance(arg, ObjectIdentifier): diff --git a/py34/bacpypes/service/cov.py b/py34/bacpypes/service/cov.py index 0bb07d5a..21fd0b60 100644 --- a/py34/bacpypes/service/cov.py +++ b/py34/bacpypes/service/cov.py @@ -8,7 +8,7 @@ from ..capability import Capability from ..core import deferred -from ..task import OneShotTask, TaskManager +from ..task import OneShotTask, RecurringFunctionTask, TaskManager from ..iocb import IOCB from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ @@ -167,6 +167,23 @@ def __init__(self, obj): # list of all active subscriptions self.cov_subscriptions = SubscriptionList() + def add_subscription(self, cov): + if _debug: COVDetection._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: COVDetection._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: COVDetection._debug(" - task suspended") + + # remove it from the subscription list for its object + self.cov_subscriptions.remove(cov) + def execute(self): if _debug: COVDetection._debug("execute") @@ -296,20 +313,20 @@ def present_value_filter(self, old_value, new_value): self.previous_reported_value = old_value # see if it changed enough to trigger reporting - value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ - or (new_value >= (self.previous_reported_value + self.covIncrement)) + value_changed = (new_value <= (self.previous_reported_value - self.obj.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.obj.covIncrement)) if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) return value_changed - def send_cov_notifications(self): - if _debug: COVIncrementCriteria._debug("send_cov_notifications") + def send_cov_notifications(self, subscription=None): + if _debug: COVIncrementCriteria._debug("send_cov_notifications %r", subscription) # when sending out notifications, keep the current value self.previous_reported_value = self.presentValue # continue - COVDetection.send_cov_notifications(self) + COVDetection.send_cov_notifications(self, subscription) class AccessDoorCriteria(COVDetection): @@ -376,17 +393,81 @@ class LoadControlCriteria(COVDetection): ) -class PulseConverterCriteria(COVDetection): +@bacpypes_debugging +class PulseConverterCriteria(COVIncrementCriteria): properties_tracked = ( 'presentValue', 'statusFlags', + 'covPeriod', ) properties_reported = ( 'presentValue', 'statusFlags', ) + def __init__(self, obj): + if _debug: PulseConverterCriteria._debug("__init__ %r", obj) + COVIncrementCriteria.__init__(self, obj) + + # check for a period + if self.covPeriod == 0: + if _debug: PulseConverterCriteria._debug(" - no periodic notifications") + self.cov_period_task = None + else: + if _debug: PulseConverterCriteria._debug(" - covPeriod: %r", self.covPeriod) + self.cov_period_task = RecurringFunctionTask(self.covPeriod * 1000, self.send_cov_notifications) + if _debug: PulseConverterCriteria._debug(" - cov period task created") + + def add_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("add_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.add_subscription(self, cov) + + # if there is a COV period task, install it + if self.cov_period_task: + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - cov period task installed") + + def cancel_subscription(self, cov): + if _debug: PulseConverterCriteria._debug("cancel_subscription %r", cov) + + # let the parent classes do their thing + COVIncrementCriteria.cancel_subscription(self, cov) + + # if there are no more subscriptions, cancel the task + if not len(self.cov_subscriptions): + if self.cov_period_task and self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - cov period task suspended") + self.cov_period_task = None + + @monitor_filter('covPeriod') + def cov_period_filter(self, old_value, new_value): + if _debug: PulseConverterCriteria._debug("cov_period_filter %r %r", old_value, new_value) + + # check for an old period + if old_value != 0: + if self.cov_period_task.isScheduled: + self.cov_period_task.suspend_task() + if _debug: PulseConverterCriteria._debug(" - canceled old task") + self.cov_period_task = None + + # check for a new period + if new_value != 0: + self.cov_period_task = RecurringFunctionTask(new_value * 1000, self.cov_period_x) + self.cov_period_task.install_task() + if _debug: PulseConverterCriteria._debug(" - new task created and installed") + + return False + + def send_cov_notifications(self, subscription=None): + if _debug: PulseConverterCriteria._debug("send_cov_notifications %r", subscription) + + # pass along to the parent class as if something changed + COVIncrementCriteria.send_cov_notifications(self, subscription) + # mapping from object type to appropriate criteria class criteria_type_map = { @@ -513,22 +594,17 @@ def __init__(self): def add_subscription(self, cov): if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) - # add it to the subscription list for its object - self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + # let the detection algorithm know this is a new or additional subscription + self.cov_detections[cov.obj_ref].add_subscription(cov) def cancel_subscription(self, cov): if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) - # cancel the subscription timeout - if cov.isScheduled: - cov.suspend_task() - if _debug: ChangeOfValueServices._debug(" - task suspended") - # get the detection algorithm object cov_detection = self.cov_detections[cov.obj_ref] - # remove it from the subscription list for its object - cov_detection.cov_subscriptions.remove(cov) + # let the detection algorithm know this subscription is going away + cov_detection.cancel_subscription(cov) # if the detection algorithm doesn't have any subscriptions, remove it if not len(cov_detection.cov_subscriptions): diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index 492a950e..d85b3a42 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -15,7 +15,7 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser -from bacpypes.core import run +from bacpypes.core import run, deferred from bacpypes.comm import bind from bacpypes.pdu import Address, LocalBroadcast @@ -219,6 +219,9 @@ def main(): # bind the router stack to the vlan network through this node router.nsap.bind(router_node, vlan_network) + # send network topology + deferred(router.nse.i_am_router_to_network) + # device identifier is assigned from the address device_instance = vlan_network * 100 + int(args.addr2) _log.debug(" - device_instance: %r", device_instance) diff --git a/samples/COVClient.py b/samples/COVClient.py index dd6f8aa0..e7816edd 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -15,9 +15,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class from bacpypes.apdu import SubscribeCOVRequest, \ SimpleAckPDU, RejectPDU, AbortPDU +from bacpypes.primitivedata import ObjectIdentifier from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -92,7 +92,7 @@ def do_UnconfirmedCOVNotificationRequest(self, apdu): class SubscribeCOVConsoleCmd(ConsoleCmd): def do_subscribe(self, args): - """subscribe addr proc_id obj_type obj_inst [ confirmed ] [ lifetime ] + """subscribe addr proc_id obj_id [ confirmed ] [ lifetime ] Generate a SubscribeCOVRequest and wait for the response. """ @@ -100,18 +100,13 @@ def do_subscribe(self, args): if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) try: - addr, proc_id, obj_type, obj_inst = args[:4] + addr, proc_id, obj_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value proc_id = int(proc_id) - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - obj_inst = int(obj_inst) - - if len(args) >= 5: - issue_confirmed = args[4] + if len(args) >= 4: + issue_confirmed = args[3] if issue_confirmed == '-': issue_confirmed = None else: @@ -120,8 +115,8 @@ def do_subscribe(self, args): else: issue_confirmed = None - if len(args) >= 6: - lifetime = args[5] + if len(args) >= 5: + lifetime = args[4] if lifetime == '-': lifetime = None else: @@ -133,7 +128,7 @@ def do_subscribe(self, args): # build a request request = SubscribeCOVRequest( subscriberProcessIdentifier=proc_id, - monitoredObjectIdentifier=(obj_type, obj_inst), + monitoredObjectIdentifier=obj_id, ) request.pduDestination = Address(addr) diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 2a268e09..8e9a85f0 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -67,10 +67,7 @@ def __init__(self, **kwargs): # see if a priority array was provided if (priorityArray not in kwargs): - new_priority_array = PriorityArray() - for i in range(16): - new_priority_array.append(PriorityValue(null=())) - setattr(self, priorityArray, new_priority_array) + setattr(self, priorityArray, PriorityArray()) # see if a present value was provided if (relinquishDefault not in kwargs): diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 64681e63..84280f4c 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -19,6 +19,7 @@ from bacpypes.pdu import Address, GlobalBroadcast from bacpypes.apdu import ReadPropertyRequest, WhoIsRequest +from bacpypes.primitivedata import ObjectIdentifier from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype @@ -65,30 +66,28 @@ def do_read(self, args): if _debug: ThreadedHTTPRequestHandler._debug("do_read %r", args) try: - addr, obj_type, obj_inst = args[:3] + addr, obj_id = args[:2] + obj_id = ObjectIdentifier(obj_id).value # get the object type - if not get_object_class(obj_type): + if not get_object_class(obj_id[0]): raise ValueError("unknown object type") - # get the instance number - obj_inst = int(obj_inst) - # implement a default property, the bain of committee meetings - if len(args) == 4: - prop_id = args[3] + if len(args) == 3: + prop_id = args[2] else: prop_id = "presentValue" # look for its datatype, an easy way to see if the property is # appropriate for the object - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) diff --git a/samples/IP2IPRouter.py b/samples/IP2IPRouter.py index 3d83f491..517040b2 100755 --- a/samples/IP2IPRouter.py +++ b/samples/IP2IPRouter.py @@ -21,7 +21,7 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser -from bacpypes.core import run +from bacpypes.core import run, deferred from bacpypes.comm import bind from bacpypes.pdu import Address @@ -115,6 +115,9 @@ def main(): router = IP2IPRouter(Address(args.addr1), args.net1, Address(args.addr2), args.net2) if _debug: _log.debug(" - router: %r", router) + # send network topology + deferred(router.nse.i_am_router_to_network) + _log.debug("running") run() diff --git a/samples/IP2VLANRouter.py b/samples/IP2VLANRouter.py index c3000b25..8fdbfd1e 100755 --- a/samples/IP2VLANRouter.py +++ b/samples/IP2VLANRouter.py @@ -15,7 +15,7 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser -from bacpypes.core import run +from bacpypes.core import run, deferred from bacpypes.comm import bind from bacpypes.pdu import Address, LocalBroadcast @@ -219,6 +219,9 @@ def main(): # bind the router stack to the vlan network through this node router.nsap.bind(router_node, vlan_network) + # send network topology + deferred(router.nse.i_am_router_to_network) + # make some devices for device_number in range(2, 2 + args.count): # device identifier is assigned from the address diff --git a/samples/MultipleReadProperty.py b/samples/MultipleReadProperty.py index 64064683..d3dd72c5 100755 --- a/samples/MultipleReadProperty.py +++ b/samples/MultipleReadProperty.py @@ -19,7 +19,7 @@ from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyRequest -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication @@ -34,8 +34,8 @@ # point list, set according to your device point_list = [ - ('10.0.1.14', 'analogValue', 1, 'presentValue'), - ('10.0.1.14', 'analogValue', 2, 'presentValue'), + ('10.0.1.14', 'analogValue:1', 'presentValue'), + ('10.0.1.14', 'analogValue:2', 'presentValue'), ] # @@ -65,11 +65,12 @@ def next_request(self): return # get the next request - addr, obj_type, obj_inst, prop_id = self.point_queue.popleft() + addr, obj_id, prop_id = self.point_queue.popleft() + obj_id = ObjectIdentifier(obj_id).value # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py index 4ad8c0d7..8da52a2b 100755 --- a/samples/MultipleReadPropertyThreaded.py +++ b/samples/MultipleReadPropertyThreaded.py @@ -35,8 +35,8 @@ # point list, set according to your device point_list = [ - ('10.0.1.14', 'analogValue', 1, 'presentValue'), - ('10.0.1.14', 'analogValue', 2, 'presentValue'), + ('10.0.1.14', 'analogValue:1', 'presentValue'), + ('10.0.1.14', 'analogValue:2', 'presentValue'), ] # @@ -61,10 +61,10 @@ def run(self): global this_application # loop through the points - for addr, obj_type, obj_inst, prop_id in self.point_queue: + for addr, obj_id, prop_id in self.point_queue: # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=ObjectIdentifer(obj_id).value, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) diff --git a/samples/NATRouter.py b/samples/NATRouter.py index fa9b70c1..c7b19d70 100755 --- a/samples/NATRouter.py +++ b/samples/NATRouter.py @@ -19,7 +19,7 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser -from bacpypes.core import run +from bacpypes.core import run, deferred from bacpypes.comm import bind from bacpypes.pdu import Address @@ -128,6 +128,9 @@ def main(): router = NATRouter(args.addr1, args.port1, args.net1, args.addr2, args.port2, args.net2) if _debug: _log.debug(" - router: %r", router) + # send network topology + deferred(router.nse.i_am_router_to_network) + _log.debug("running") run() diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index 3eff2bfa..a9de3c3f 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -17,11 +17,11 @@ from bacpypes.pdu import Address from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.local.device import LocalDeviceObject # some debugging @@ -45,28 +45,22 @@ def do_read(self, args): if _debug: ReadPropertyConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(obj_inst) - - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadPropertyConsoleCmd._debug(" - request: %r", request) # make an IOCB diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index ff1c042f..a14cf762 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -17,10 +17,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class from bacpypes.apdu import ReadPropertyRequest -from bacpypes.primitivedata import Tag +from bacpypes.primitivedata import Tag, ObjectIdentifier from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -41,29 +40,23 @@ class ReadPropertyAnyConsoleCmd(ConsoleCmd): def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: ReadPropertyAnyConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(obj_inst) + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadPropertyAnyConsoleCmd._debug(" - request: %r", request) # make an IOCB diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index 35456b78..b47bf447 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -16,11 +16,11 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyMultipleRequest, PropertyReference, \ ReadAccessSpecification, ReadPropertyMultipleACK -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.basetypes import PropertyIdentifier @@ -42,7 +42,7 @@ class ReadPropertyMultipleConsoleCmd(ConsoleCmd): def do_read(self, args): - """read ( ( [ ] )... )...""" + """read ( ( [ ] )... )...""" args = args.split() if _debug: ReadPropertyMultipleConsoleCmd._debug("do_read %r", args) @@ -53,15 +53,7 @@ def do_read(self, args): read_access_spec_list = [] while i < len(args): - obj_type = args[i] - i += 1 - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(args[i]) + obj_id = ObjectIdentifier(args[i]).value i += 1 prop_reference_list = [] @@ -74,7 +66,7 @@ def do_read(self, args): if prop_id in ('all', 'required', 'optional'): pass else: - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") @@ -97,7 +89,7 @@ def do_read(self, args): # build a read access specification read_access_spec = ReadAccessSpecification( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, listOfPropertyReferences=prop_reference_list, ) diff --git a/samples/ReadRange.py b/samples/ReadRange.py index 53f77aa8..0eca99b7 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -16,10 +16,11 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.apdu import ReadRangeRequest, ReadRangeACK from bacpypes.app import BIPSimpleApplication +from bacpypes.primitivedata import ObjectIdentifier from bacpypes.local.device import LocalDeviceObject # some debugging @@ -37,33 +38,27 @@ class ReadRangeConsoleCmd(ConsoleCmd): def do_readrange(self, args): - """readrange [ ]""" + """readrange [ ]""" args = args.split() if _debug: ReadRangeConsoleCmd._debug("do_readrange %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(obj_inst) - - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadRangeRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadRangeConsoleCmd._debug(" - request: %r", request) # make an IOCB diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py index ac89da19..201b6f7f 100644 --- a/samples/ReadWriteEventMessageTexts.py +++ b/samples/ReadWriteEventMessageTexts.py @@ -19,7 +19,7 @@ from bacpypes.apdu import SimpleAckPDU, \ ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest -from bacpypes.primitivedata import Unsigned, CharacterString +from bacpypes.primitivedata import Unsigned, CharacterString, ObjectIdentifier from bacpypes.constructeddata import Array, ArrayOf, Any from bacpypes.app import BIPSimpleApplication @@ -47,16 +47,17 @@ def do_read(self, args): global context try: - addr, obj_type, obj_inst = context + addr, obj_id = context + obj_id = ObjectIdentifier(obj_id).value prop_id = 'eventMessageTexts' - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) @@ -122,7 +123,8 @@ def do_write(self, args): ReadWritePropertyConsoleCmd._debug("do_write %r", args) try: - addr, obj_type, obj_inst = context + addr, obj_id = context + obj_id = ObjectIdentifier(obj_id).value prop_id = 'eventMessageTexts' indx = None @@ -137,7 +139,7 @@ def do_write(self, args): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id ) request.pduDestination = Address(addr) @@ -208,12 +210,8 @@ def main(): help="address of server", ) parser.add_argument( - "objtype", - help="object type", - ) - parser.add_argument( - "objinst", type=int, - help="object instance", + "objid", + help="object identifier", ) args = parser.parse_args() @@ -221,7 +219,7 @@ def main(): if _debug: _log.debug(" - args: %r", args) # set the context, the collection of the above parameters - context = args.address, args.objtype, args.objinst + context = args.address, args.objid if _debug: _log.debug(" - context: %r", context) # make a device object diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index 9458f9cc..12fed182 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -18,12 +18,12 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.apdu import SimpleAckPDU, \ ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest from bacpypes.primitivedata import Null, Atomic, Boolean, Unsigned, Integer, \ - Real, Double, OctetString, CharacterString, BitString, Date, Time + Real, Double, OctetString, CharacterString, BitString, Date, Time, ObjectIdentifier from bacpypes.constructeddata import Array, Any, AnyAtomic from bacpypes.app import BIPSimpleApplication @@ -44,33 +44,27 @@ class ReadWritePropertyConsoleCmd(ConsoleCmd): def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: ReadWritePropertyConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(obj_inst) - - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) # make an IOCB @@ -126,32 +120,30 @@ def do_write(self, args): ReadWritePropertyConsoleCmd._debug("do_write %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - if obj_type.isdigit(): - obj_type = int(obj_type) - obj_inst = int(obj_inst) - value = args[4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value + value = args[3] indx = None - if len(args) >= 6: - if args[5] != "-": - indx = int(args[5]) + if len(args) >= 5: + if args[4] != "-": + indx = int(args[4]) if _debug: ReadWritePropertyConsoleCmd._debug(" - indx: %r", indx) priority = None - if len(args) >= 7: - priority = int(args[6]) + if len(args) >= 6: + priority = int(args[5]) if _debug: ReadWritePropertyConsoleCmd._debug(" - priority: %r", priority) # get the datatype - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if _debug: ReadWritePropertyConsoleCmd._debug(" - datatype: %r", datatype) # change atomic values into something encodeable, null is a special case if (value == 'null'): value = Null() elif issubclass(datatype, AnyAtomic): - dtype, dvalue = value.split(':') + dtype, dvalue = value.split(':', 1) if _debug: ReadWritePropertyConsoleCmd._debug(" - dtype, dvalue: %r, %r", dtype, dvalue) datatype = { @@ -165,6 +157,7 @@ def do_write(self, args): 'bs': BitString, 'date': Date, 'time': Time, + 'id': ObjectIdentifier, }[dtype] if _debug: ReadWritePropertyConsoleCmd._debug(" - datatype: %r", datatype) @@ -192,7 +185,7 @@ def do_write(self, args): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id ) request.pduDestination = Address(addr) diff --git a/samples/RecurringMultipleReadProperty.py b/samples/RecurringMultipleReadProperty.py index a479089e..05d135c6 100755 --- a/samples/RecurringMultipleReadProperty.py +++ b/samples/RecurringMultipleReadProperty.py @@ -20,7 +20,7 @@ from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyRequest -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication @@ -32,8 +32,8 @@ # point list point_list = [ - ('1.2.3.4', 'analogValue', 1, 'presentValue'), - ('1.2.3.4', 'analogValue', 2, 'presentValue'), + ('1.2.3.4', 'analogValue:1', 'presentValue'), + ('1.2.3.4', 'analogValue:2', 'presentValue'), ] # @@ -92,11 +92,12 @@ def next_request(self): return # get the next request - addr, obj_type, obj_inst, prop_id = self.point_queue.popleft() + addr, obj_id, prop_id = self.point_queue.popleft() + obj_id = ObjectIdentifier(obj_id).value # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) diff --git a/samples/ThreadedReadProperty.py b/samples/ThreadedReadProperty.py index 6f79d717..bdcdc54b 100755 --- a/samples/ThreadedReadProperty.py +++ b/samples/ThreadedReadProperty.py @@ -20,7 +20,7 @@ from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyRequest -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication @@ -36,12 +36,12 @@ # point list, set according to your devices point_list = [ ('10.0.1.14', [ - ('analogValue', 1, 'presentValue'), - ('analogValue', 2, 'presentValue'), + ('analogValue:1', 'presentValue'), + ('analogValue:2', 'presentValue'), ]), ('10.0.1.15', [ - ('analogValue', 1, 'presentValue'), - ('analogValue', 2, 'presentValue'), + ('analogValue:1', 'presentValue'), + ('analogValue:2', 'presentValue'), ]), ] @@ -70,11 +70,13 @@ def run(self): global this_application # loop through the points - for obj_type, obj_inst, prop_id in self.point_list: + for obj_id, prop_id in self.point_list: + obj_id = ObjectIdentifier(obj_id).value + # build a request request = ReadPropertyRequest( destination=self.device_address, - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) if _debug: ReadPointListThread._debug(" - request: %r", request) diff --git a/samples/VendorReadWriteProperty.py b/samples/VendorReadWriteProperty.py index 2d64493f..b82e03a5 100755 --- a/samples/VendorReadWriteProperty.py +++ b/samples/VendorReadWriteProperty.py @@ -21,7 +21,7 @@ from bacpypes.object import get_object_class, get_datatype from bacpypes.apdu import ReadPropertyRequest, WritePropertyRequest -from bacpypes.primitivedata import Tag, Null, Atomic, Integer, Unsigned, Real +from bacpypes.primitivedata import Tag, Null, Atomic, Integer, Unsigned, Real, ObjectIdentifier from bacpypes.constructeddata import Array, Any from bacpypes.app import BIPSimpleApplication @@ -44,39 +44,31 @@ class ReadWritePropertyConsoleCmd(ConsoleCmd): def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: ReadWritePropertyConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type, VendorAVObject.vendor_id): - raise ValueError("unknown object type") - if _debug: ReadWritePropertyConsoleCmd._debug(" - obj_type: %r", obj_type) - - obj_inst = int(obj_inst) - if _debug: ReadWritePropertyConsoleCmd._debug(" - obj_inst: %r", obj_inst) + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value if prop_id.isdigit(): prop_id = int(prop_id) if _debug: ReadWritePropertyConsoleCmd._debug(" - prop_id: %r", prop_id) - datatype = get_datatype(obj_type, prop_id, VendorAVObject.vendor_id) + datatype = get_datatype(obj_id[0], prop_id, VendorAVObject.vendor_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) # make an IOCB @@ -124,41 +116,36 @@ def do_read(self, args): ReadWritePropertyConsoleCmd._exception("exception: %r", error) def do_write(self, args): - """write [ ] [ ]""" + """write [ ] [ ]""" args = args.split() ReadWritePropertyConsoleCmd._debug("do_write %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type, VendorAVObject.vendor_id): + if not get_object_class(obj_id[0], VendorAVObject.vendor_id): raise ValueError("unknown object type") - if _debug: ReadWritePropertyConsoleCmd._debug(" - obj_type: %r", obj_type) - - obj_inst = int(obj_inst) - if _debug: ReadWritePropertyConsoleCmd._debug(" - obj_inst: %r", obj_inst) if prop_id.isdigit(): prop_id = int(prop_id) if _debug: ReadWritePropertyConsoleCmd._debug(" - prop_id: %r", prop_id) - value = args[4] + value = args[3] indx = None - if len(args) >= 6: - if args[5] != "-": - indx = int(args[5]) + if len(args) >= 5: + if args[4] != "-": + indx = int(args[4]) if _debug: ReadWritePropertyConsoleCmd._debug(" - indx: %r", indx) priority = None - if len(args) >= 7: - priority = int(args[6]) + if len(args) >= 6: + priority = int(args[5]) if _debug: ReadWritePropertyConsoleCmd._debug(" - priority: %r", priority) # get the datatype - datatype = get_datatype(obj_type, prop_id, VendorAVObject.vendor_id) + datatype = get_datatype(obj_id[0], prop_id, VendorAVObject.vendor_id) if _debug: ReadWritePropertyConsoleCmd._debug(" - datatype: %r", datatype) # change atomic values into something encodeable, null is a special case @@ -185,7 +172,7 @@ def do_write(self, args): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id ) request.pduDestination = Address(addr) diff --git a/samples/WhoIsRouter.py b/samples/WhoIsRouter.py index 9bd482d4..b698a93a 100755 --- a/samples/WhoIsRouter.py +++ b/samples/WhoIsRouter.py @@ -12,7 +12,10 @@ from bacpypes.core import run, enable_sleeping from bacpypes.pdu import Address -from bacpypes.npdu import InitializeRoutingTable, WhoIsRouterToNetwork, IAmRouterToNetwork +from bacpypes.npdu import ( + WhoIsRouterToNetwork, IAmRouterToNetwork, + InitializeRoutingTable, InitializeRoutingTableAck, + ) from bacpypes.app import BIPNetworkApplication @@ -45,6 +48,11 @@ def indication(self, adapter, npdu): if isinstance(npdu, IAmRouterToNetwork): print("{} -> {}, {}".format(npdu.pduSource, npdu.pduDestination, npdu.iartnNetworkList)) + elif isinstance(npdu, InitializeRoutingTableAck): + print("{} routing table".format(npdu.pduSource)) + for rte in npdu.irtaTable: + print(" {} {} {}".format(rte.rtDNET, rte.rtPortID, rte.rtPortInfo)) + BIPNetworkApplication.indication(self, adapter, npdu) def response(self, adapter, npdu): @@ -76,7 +84,7 @@ def do_irt(self, args): return # give it to the application - this_application.request(this_application.nsap.adapters[None], request) + this_application.request(this_application.nsap.local_adapter, request) def do_wirtn(self, args): """wirtn [ ]""" @@ -94,7 +102,7 @@ def do_wirtn(self, args): return # give it to the application - this_application.request(this_application.nsap.adapters[None], request) + this_application.request(this_application.nsap.local_adapter, request) # # __main__ diff --git a/samples/WritePropertyTCPServer.py b/samples/WritePropertyTCPServer.py index 8a4f613b..29ffd29c 100755 --- a/samples/WritePropertyTCPServer.py +++ b/samples/WritePropertyTCPServer.py @@ -20,7 +20,7 @@ from bacpypes.object import get_datatype from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU -from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real +from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real, ObjectIdentifier from bacpypes.constructeddata import Array, Any from bacpypes.app import BIPSimpleApplication @@ -55,25 +55,23 @@ def confirmation(self, pdu): if _debug: WritePropertyClient._debug(" - args: %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - if obj_type.isdigit(): - obj_type = int(obj_type) - obj_inst = int(obj_inst) - value = args[4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value + value = args[3] indx = None - if len(args) >= 6: - if args[5] != "-": - indx = int(args[5]) + if len(args) >= 5: + if args[4] != "-": + indx = int(args[4]) if _debug: WritePropertyClient._debug(" - indx: %r", indx) priority = None - if len(args) >= 7: - priority = int(args[6]) + if len(args) >= 6: + priority = int(args[5]) if _debug: WritePropertyClient._debug(" - priority: %r", priority) # get the datatype - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if _debug: WritePropertyClient._debug(" - datatype: %r", datatype) # change atomic values into something encodeable, null is a special case @@ -100,7 +98,7 @@ def confirmation(self, pdu): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id ) request.pduDestination = Address(addr) diff --git a/samples/WriteSomething.py b/samples/WriteSomething.py index 529399eb..bbed9166 100755 --- a/samples/WriteSomething.py +++ b/samples/WriteSomething.py @@ -21,7 +21,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject -from bacpypes.primitivedata import TagList, OpeningTag, ClosingTag, ContextTag +from bacpypes.primitivedata import TagList, OpeningTag, ClosingTag, ContextTag, ObjectIdentifier from bacpypes.constructeddata import Any from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU @@ -40,26 +40,24 @@ class WriteSomethingConsoleCmd(ConsoleCmd): def do_write(self, args): - """write [ ]""" + """write [ ]""" args = args.split() if _debug: WriteSomethingConsoleCmd._debug("do_write %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - - obj_type = int(obj_type) - obj_inst = int(obj_inst) + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value prop_id = int(prop_id) # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) # build a custom datastructure tag_list = TagList([ diff --git a/sandbox/xtest_issue_45.py b/sandbox/xtest_issue_45.py index a0b6a326..f32d13da 100755 --- a/sandbox/xtest_issue_45.py +++ b/sandbox/xtest_issue_45.py @@ -14,20 +14,25 @@ from bacpypes.core import run from bacpypes.comm import bind -from bacpypes.pdu import Address, GlobalBroadcast +from bacpypes.pdu import Address, LocalBroadcast, GlobalBroadcast from bacpypes.vlan import Network, Node -from bacpypes.app import LocalDeviceObject, Application +from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.local.device import LocalDeviceObject from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, WritePropertyRequest -from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real +from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real, ObjectIdentifier from bacpypes.constructeddata import Array, Any from bacpypes.object import get_object_class, get_datatype +# basic services +from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -41,11 +46,11 @@ # @bacpypes_debugging -class VLANApplication(Application): +class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): - def __init__(self, vlan_device, vlan_address, aseID=None): - if _debug: VLANApplication._debug("__init__ %r %r aseID=%r", vlan_device, vlan_address, aseID) - Application.__init__(self, vlan_device, vlan_address, aseID) + def __init__(self, vlan_device, vlan_address): + if _debug: VLANApplication._debug("__init__ %r %r", vlan_device, vlan_address) + Application.__init__(self, vlan_device, vlan_address) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -54,6 +59,10 @@ def __init__(self, vlan_device, vlan_address, aseID=None): # can know if it should support segmentation self.smap = StateMachineAccessPoint(vlan_device) + # 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() @@ -141,33 +150,27 @@ def do_iam(self, args): TestConsoleCmd._exception("exception: %r", e) def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: TestConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError, "unknown object type" + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - obj_inst = int(obj_inst) - - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError, "invalid property for object type" # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: TestConsoleCmd._debug(" - request: %r", request) # give it to the application @@ -177,30 +180,28 @@ def do_read(self, args): TestConsoleCmd._exception("exception: %r", e) def do_write(self, args): - """write [ ] [ ]""" + """write [ ] [ ]""" args = args.split() if _debug: TestConsoleCmd._debug("do_write %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] - if obj_type.isdigit(): - obj_type = int(obj_type) - obj_inst = int(obj_inst) + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value value = args[4] indx = None - if len(args) >= 6: - if args[5] != "-": - indx = int(args[5]) + if len(args) >= 5: + if args[4] != "-": + indx = int(args[4]) if _debug: TestConsoleCmd._debug(" - indx: %r", indx) priority = None - if len(args) >= 7: - priority = int(args[6]) + if len(args) >= 6: + priority = int(args[5]) if _debug: TestConsoleCmd._debug(" - priority: %r", priority) # get the datatype - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if _debug: TestConsoleCmd._debug(" - datatype: %r", datatype) # change atomic values into something encodeable, null is a special case @@ -227,7 +228,7 @@ def do_write(self, args): # build a request request = WritePropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id ) request.pduDestination = Address(addr) @@ -269,7 +270,7 @@ def do_write(self, args): if _debug: _log.debug(" - args: %r", args) # create a VLAN - vlan = Network() + vlan = Network(broadcast_address=LocalBroadcast()) # create the first device vlan_device_1 = \ diff --git a/tests/state_machine.py b/tests/state_machine.py index c70e2dd3..9bc63d59 100755 --- a/tests/state_machine.py +++ b/tests/state_machine.py @@ -919,7 +919,9 @@ def after_receive(self, pdu): def unexpected_receive(self, pdu): """Called with PDU that did not match. Unless this is trapped by the state, the default behaviour is to fail.""" - if _debug: StateMachine._debug("unexpected_receive(%s) %r", self.name, pdu) + if _debug: + StateMachine._debug("unexpected_receive(%s) %r", self.name, pdu) + StateMachine._debug(" - current_state: %r", self.current_state) # go to the unexpected receive state (failing) self.goto_state(self.unexpected_receive_state) diff --git a/tests/test_constructed_data/test_array_of.py b/tests/test_constructed_data/test_array_of.py index 8e848332..e448254c 100644 --- a/tests/test_constructed_data/test_array_of.py +++ b/tests/test_constructed_data/test_array_of.py @@ -8,11 +8,11 @@ import unittest -from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.errors import MissingRequiredParameter -from bacpypes.primitivedata import Integer, Tag, TagList -from bacpypes.constructeddata import Element, Sequence, ArrayOf +from bacpypes.primitivedata import TagList, Integer, Time +from bacpypes.constructeddata import ArrayOf +from bacpypes.basetypes import TimeStamp from .helpers import SimpleSequence @@ -46,7 +46,7 @@ def test_empty_array(self): # create another sequence and decode the tag list ary = IntegerArray() ary.decode(tag_list) - if _debug: TestIntegerArray._debug(" - seq: %r", seq) + if _debug: TestIntegerArray._debug(" - ary: %r", ary) def test_append(self): if _debug: TestIntegerArray._debug("test_append") @@ -86,7 +86,7 @@ def test_index_item(self): # not find something with self.assertRaises(ValueError): - indx = ary.index(4) + ary.index(4) def test_remove_item(self): if _debug: TestIntegerArray._debug("test_remove_item") @@ -163,6 +163,132 @@ def test_codec(self): assert ary.value[1:] == ary_value +# fixed length array of integers +IntegerArray5 = ArrayOf(Integer, fixed_length=5) + +@bacpypes_debugging +class TestIntegerArray5(unittest.TestCase): + + def test_empty_array(self): + if _debug: TestIntegerArray5._debug("test_empty_array") + + # create an empty array + ary = IntegerArray5() + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # array sematics + assert len(ary) == 5 + assert ary[0] == 5 + + # value correct + assert ary.value[1:] == [0, 0, 0, 0, 0] + + def test_append(self): + if _debug: TestIntegerArray5._debug("test_append") + + # create an empty array + ary = IntegerArray5() + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # append an integer + with self.assertRaises(TypeError): + ary.append(2) + + def test_delete_item(self): + if _debug: TestIntegerArray5._debug("test_delete_item") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # delete something + with self.assertRaises(TypeError): + del ary[2] + + def test_index_item(self): + if _debug: TestIntegerArray5._debug("test_index_item") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # find something + assert ary.index(3) == 3 + + # not find something + with self.assertRaises(ValueError): + ary.index(100) + + def test_remove_item(self): + if _debug: TestIntegerArray5._debug("test_remove_item") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # remove something + with self.assertRaises(TypeError): + ary.remove(4) + + def test_resize(self): + if _debug: TestIntegerArray5._debug("test_resize") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # make it the same length (noop) + ary[0] = 5 + + # changing it to something else fails + with self.assertRaises(TypeError): + ary[0] = 4 + + def test_get_item(self): + if _debug: TestIntegerArray5._debug("test_get_item") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # BACnet semantics + assert ary[1] == 1 + + def test_set_item(self): + if _debug: TestIntegerArray5._debug("test_set_item") + + # create an array + ary = IntegerArray5([1, 2, 3, 4, 5]) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # BACnet semantics, no type checking + ary[1] = 10 + assert ary[1] == 10 + + def test_codec(self): + if _debug: TestIntegerArray5._debug("test_codec") + + # test array contents + ary_value = [1, 2, 3, 4, 5] + + # create an array + ary = IntegerArray5(ary_value) + if _debug: TestIntegerArray5._debug(" - ary: %r", ary) + + # encode it in a tag list + tag_list = TagList() + ary.encode(tag_list) + if _debug: TestIntegerArray5._debug(" - tag_list: %r", tag_list) + + # create another sequence and decode the tag list + ary = IntegerArray() + ary.decode(tag_list) + if _debug: TestIntegerArray5._debug(" - ary %r", ary) + + # value matches + assert ary.value[1:] == ary_value + + # array of a sequence SimpleSequenceArray = ArrayOf(SimpleSequence) @@ -196,3 +322,23 @@ def test_codec(self): # value matches assert ary.value[1:] == ary_value + +# fixed length array of TimeStamps +ArrayOfTimeStamp = ArrayOf(TimeStamp, fixed_length=16, + prototype=TimeStamp(time=Time().value), + ) + +@bacpypes_debugging +class TestArrayOfTimeStamp(unittest.TestCase): + + def test_empty_array(self): + if _debug: TestArrayOfTimeStamp._debug("test_empty_array") + + # create an empty array + ary = ArrayOfTimeStamp() + if _debug: TestArrayOfTimeStamp._debug(" - ary: %r", ary) + + # array sematics + assert len(ary) == 16 + assert ary[0] == 16 + diff --git a/tests/test_network/__init__.py b/tests/test_network/__init__.py index b08960d4..b0f72021 100644 --- a/tests/test_network/__init__.py +++ b/tests/test_network/__init__.py @@ -8,4 +8,6 @@ from . import test_net_2 from . import test_net_3 from . import test_net_4 +from . import test_net_5 +from . import test_net_6 diff --git a/tests/test_network/helpers.py b/tests/test_network/helpers.py index a409ffb6..cb4f2672 100644 --- a/tests/test_network/helpers.py +++ b/tests/test_network/helpers.py @@ -20,7 +20,7 @@ from bacpypes.service.device import WhoIsIAmServices from bacpypes.service.object import ReadWritePropertyServices -from ..state_machine import ClientStateMachine +from ..state_machine import StateMachine, ClientStateMachine # some debugging _debug = 0 @@ -109,11 +109,11 @@ def __init__(self, address, vlan): # create a network layer encoder/decoder self.codec = NPDUCodec() - if _debug: SnifferStateMachine._debug(" - codec: %r", self.codec) + if _debug: NetworkLayerStateMachine._debug(" - codec: %r", self.codec) # create a node, added to the network self.node = Node(self.address, vlan) - if _debug: SnifferStateMachine._debug(" - node: %r", self.node) + if _debug: NetworkLayerStateMachine._debug(" - node: %r", self.node) # bind this to the node bind(self, self.codec, self.node) @@ -148,6 +148,19 @@ def add_network(self, address, vlan, net): # bind the BIP stack to the local network self.nsap.bind(node, net) +# +# RouterStateMachine +# + +@bacpypes_debugging +class RouterStateMachine(RouterNode, StateMachine): + + def __init__(self): + if _debug: RouterStateMachine._debug("__init__") + + RouterNode.__init__(self) + StateMachine.__init__(self) + # # TestDeviceObject # diff --git a/tests/test_network/test_net_5.py b/tests/test_network/test_net_5.py new file mode 100644 index 00000000..9be76780 --- /dev/null +++ b/tests/test_network/test_net_5.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test I-Am-Router-To-Network API +------------------------------- + +Network 1 has sniffer1, the TD is on network 2 with sniffer2, network 3 has +sniffer3. Network 1 and 2 are connected with a router, network 2 and 3 +are connected by a different router. +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast +from bacpypes.vlan import Network + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..state_machine import match_pdu, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import NetworkLayerStateMachine, RouterStateMachine + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self): + if _debug: TNetwork._debug("__init__") + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # implementation under test + self.iut = RouterStateMachine() + self.append(self.iut) + + # make a little LAN + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + self.vlan1.traffic_log = self.traffic_log + + # sniffer node + self.sniffer1 = NetworkLayerStateMachine("1", self.vlan1) + self.append(self.sniffer1) + + # make another little LAN + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + self.vlan2.traffic_log = self.traffic_log + + # sniffer node + self.sniffer2 = NetworkLayerStateMachine("3", self.vlan2) + self.append(self.sniffer2) + + # make another little LAN + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + self.vlan3.traffic_log = self.traffic_log + + # sniffer node + self.sniffer3 = NetworkLayerStateMachine("4", self.vlan3) + self.append(self.sniffer3) + + # connect the vlans to the router + self.iut.add_network("5", self.vlan1, 1) + self.iut.add_network("6", self.vlan2, 2) + self.iut.add_network("7", self.vlan3, 3) + + def run(self, time_limit=60.0): + if _debug: TNetwork._debug("run %r", time_limit) + + # run the group + super(TNetwork, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: + TNetwork._debug(" - time machine finished") + + # list the state machines which shows their current state + for state_machine in self.state_machines: + TNetwork._debug(" - machine: %r", state_machine) + + # each one has a list of sent/received pdus + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + assert all_success + + +@bacpypes_debugging +class TestSimple(unittest.TestCase): + + def test_idle(self): + """Test an idle network, nothing happens is success.""" + if _debug: TestSimple._debug("test_idle") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.iut.start_state.success() + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + + +@bacpypes_debugging +class TestIAmRouterToNetwork(unittest.TestCase): + + def test_01(self): + """Test sending complete path to all adapters.""" + if _debug: TestIAmRouterToNetwork._debug("test_01") + + # create a network + tnet = TNetwork() + + # test device sends request + tnet.iut.start_state.doc("1-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network).doc("1-1-1") \ + .success() + + # network 1 sees router to networks 2 and 3 + tnet.sniffer1.start_state.doc("1-2-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2, 3], + ).doc("1-2-1") \ + .success() + + # network 2 sees router to networks 1 and 3 + tnet.sniffer2.start_state.doc("1-3-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1, 3], + ).doc("1-3-1") \ + .success() + + # network 3 sees router to networks 1 and 2 + tnet.sniffer3.start_state.doc("1-4-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1, 2], + ).doc("1-4-1") \ + .success() + + # run the group + tnet.run() + + def test_02(self): + """Test sending to a specific adapter.""" + if _debug: TestIAmRouterToNetwork._debug("test_02") + + # create a network + tnet = TNetwork() + + # extract the adapter to network 1 + net_1_adapter = tnet.iut.nsap.adapters[1] + if _debug: TestIAmRouterToNetwork._debug(" - net_1_adapter: %r", net_1_adapter) + + # test device sends request + tnet.iut.start_state.doc("2-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network, adapter=net_1_adapter).doc("2-1-1") \ + .success() + + # network 1 sees router to networks 2 and 3 + tnet.sniffer1.start_state.doc("2-2-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2, 3], + ).doc("2-2-1") \ + .success() + + # network 2 sees nothing + tnet.sniffer2.start_state.doc("2-3-0") \ + .timeout(10).doc("2-3-1") \ + .success() + + # network 3 sees nothing + tnet.sniffer3.start_state.doc("2-4-0") \ + .timeout(10).doc("2-4-1") \ + .success() + + # run the group + tnet.run() + + def test_03(self): + """Test sending to a network broadcast address.""" + if _debug: TestIAmRouterToNetwork._debug("test_03") + + # create a network + tnet = TNetwork() + + # test device sends request + tnet.iut.start_state.doc("3-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network, + destination=Address("1:*"), + ).doc("3-1-1") \ + .success() + + # network 1 sees router to networks 2 and 3 + tnet.sniffer1.start_state.doc("3-2-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2, 3], + ).doc("3-2-1") \ + .success() + + # network 2 sees nothing + tnet.sniffer2.start_state.doc("3-3-0") \ + .timeout(10).doc("3-3-1") \ + .success() + + # network 3 sees nothing + tnet.sniffer3.start_state.doc("3-4-0") \ + .timeout(10).doc("3-4-1") \ + .success() + + # run the group + tnet.run() + + def test_04(self): + """Test sending a specific network to all other networks.""" + if _debug: TestIAmRouterToNetwork._debug("test_04") + + # create a network + tnet = TNetwork() + + # test device sends request + tnet.iut.start_state.doc("4-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network, network=1).doc("4-1-1") \ + .success() + + # network 1 sees nothing + tnet.sniffer1.start_state.doc("4-2-0") \ + .timeout(10).doc("4-2-1") \ + .success() + + # network 2 sees router to network 1 + tnet.sniffer2.start_state.doc("4-3-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1], + ).doc("4-3-1") \ + .success() + + # network 3 sees router to network 1 + tnet.sniffer3.start_state.doc("4-4-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1], + ).doc("4-4-1") \ + .success() + + # run the group + tnet.run() + + def test_05(self): + """Test sending a specific network to a specific address, in this + case sending to the sniffer on network 2.""" + if _debug: TestIAmRouterToNetwork._debug("test_05") + + # create a network + tnet = TNetwork() + + # test device sends request + tnet.iut.start_state.doc("5-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network, + destination=Address("2:3"), + network=1, + ).doc("5-1-1") \ + .success() + + # network 1 sees nothing + tnet.sniffer1.start_state.doc("5-2-0") \ + .timeout(10).doc("5-2-1") \ + .success() + + # network 2 sees router to network 1 + tnet.sniffer2.start_state.doc("5-3-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1], + ).doc("5-3-1") \ + .success() + + # network 3 sees nothing + tnet.sniffer3.start_state.doc("5-4-0") \ + .timeout(10).doc("5-4-1") \ + .success() + + # run the group + tnet.run() + + def test_06(self): + """Similar to test_05, sending a specific network to a specific + address, in this case sending to the sniffer on network 2 by + providing an adapter and a local address.""" + if _debug: TestIAmRouterToNetwork._debug("test_06") + + # create a network + tnet = TNetwork() + + # extract the adapter to network 1 + net_2_adapter = tnet.iut.nsap.adapters[2] + if _debug: TestIAmRouterToNetwork._debug(" - net_2_adapter: %r", net_2_adapter) + + # test device sends request + tnet.iut.start_state.doc("6-1-0") \ + .call(tnet.iut.nse.i_am_router_to_network, + adapter=net_2_adapter, + destination=Address("3"), + network=1, + ).doc("6-1-1") \ + .success() + + # network 1 sees nothing + tnet.sniffer1.start_state.doc("6-2-0") \ + .timeout(10).doc("6-2-1") \ + .success() + + # network 2 sees router to network 1 + tnet.sniffer2.start_state.doc("6-3-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1], + ).doc("6-3-1") \ + .success() + + # network 3 sees nothing + tnet.sniffer3.start_state.doc("6-4-0") \ + .timeout(10).doc("6-4-1") \ + .success() + + # run the group + tnet.run() + diff --git a/tests/test_network/test_net_6.py b/tests/test_network/test_net_6.py new file mode 100644 index 00000000..17448f85 --- /dev/null +++ b/tests/test_network/test_net_6.py @@ -0,0 +1,454 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Network Number Discovery +----------------------------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.core import deferred +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast +from bacpypes.vlan import Network + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..state_machine import match_pdu, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork1 +# + +@bacpypes_debugging +class TNetwork1(StateMachineGroup): + + """ + Network 1 has sniffer1, the TD is on network 2 with sniffer2, network 3 has + sniffer3. Network 1 and 2 are connected with a router IUT1, network 2 and 3 + are connected by router IUT2. + """ + + def __init__(self): + if _debug: TNetwork1._debug("__init__") + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork1._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # implementation under test + self.iut1 = RouterNode() # router from vlan1 to vlan2 + self.iut2 = RouterNode() # router from vlan2 to vlan3 + + # make a little LAN + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + self.vlan1.traffic_log = self.traffic_log + + # sniffer node + self.sniffer1 = NetworkLayerStateMachine("1", self.vlan1) + self.append(self.sniffer1) + + # connect vlan1 to iut1 + self.iut1.add_network("2", self.vlan1, 1) + + # make another little LAN + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + self.vlan2.traffic_log = self.traffic_log + + # test device + self.td = NetworkLayerStateMachine("3", self.vlan2) + self.append(self.td) + + # sniffer node + self.sniffer2 = NetworkLayerStateMachine("4", self.vlan2) + self.append(self.sniffer2) + + # connect vlan2 to both routers + self.iut1.add_network("5", self.vlan2, 2) + self.iut2.add_network("6", self.vlan2, 2) + + # make another little LAN + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + self.vlan3.traffic_log = self.traffic_log + + # sniffer node + self.sniffer3 = NetworkLayerStateMachine("7", self.vlan3) + self.append(self.sniffer3) + + # connect vlan3 to the second router + self.iut2.add_network("8", self.vlan3, 3) + + if _debug: + TNetwork1._debug(" - iut1.nsap: %r", self.iut1.nsap) + TNetwork1._debug(" - iut2.nsap: %r", self.iut2.nsap) + + def run(self, time_limit=60.0): + if _debug: TNetwork1._debug("run %r", time_limit) + + # run the group + super(TNetwork1, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: + TNetwork1._debug(" - time machine finished") + + # list the state machines which shows their current state + for state_machine in self.state_machines: + TNetwork1._debug(" - machine: %r", state_machine) + + # each one has a list of sent/received pdus + for direction, pdu in state_machine.transaction_log: + TNetwork1._debug(" %s %s %s", + direction, + pdu.pduSource or pdu.pduDestination, + pdu.__class__.__name__, + ) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork1._debug) + + # check for success + all_success, some_failed = super(TNetwork1, self).check_for_success() + assert all_success + + +# +# TNetwork2 +# + +@bacpypes_debugging +class TNetwork2(StateMachineGroup): + + """ + This test network is almost exactly the same as TNetwork1 with the + exception that IUT1 is connected to network 2 but doesn't know the + network number, it learns it from IUT2. + """ + + def __init__(self): + if _debug: TNetwork2._debug("__init__") + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork2._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # implementation under test + self.iut1 = RouterNode() # router from vlan1 to vlan2 + self.iut2 = RouterNode() # router from vlan2 to vlan3 + + # make a little LAN + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + self.vlan1.traffic_log = self.traffic_log + + # sniffer node + self.sniffer1 = NetworkLayerStateMachine("1", self.vlan1) + self.append(self.sniffer1) + + # connect vlan1 to iut1 + self.iut1.add_network("2", self.vlan1, 1) + + # make another little LAN + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + self.vlan2.traffic_log = self.traffic_log + + # test device + self.td = NetworkLayerStateMachine("3", self.vlan2) + self.append(self.td) + + # sniffer node + self.sniffer2 = NetworkLayerStateMachine("4", self.vlan2) + self.append(self.sniffer2) + + # connect vlan2 to both routers + self.iut1.add_network("5", self.vlan2, None) ### NOT CONFIGURED + self.iut2.add_network("6", self.vlan2, 2) + + # make another little LAN + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + self.vlan3.traffic_log = self.traffic_log + + # sniffer node + self.sniffer3 = NetworkLayerStateMachine("7", self.vlan3) + self.append(self.sniffer3) + + # connect vlan3 to the second router + self.iut2.add_network("8", self.vlan3, 3) + + if _debug: + TNetwork2._debug(" - iut1.nsap: %r", self.iut1.nsap) + TNetwork2._debug(" - iut2.nsap: %r", self.iut2.nsap) + + def run(self, time_limit=60.0): + if _debug: TNetwork2._debug("run %r", time_limit) + + # run the group + super(TNetwork2, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: + TNetwork2._debug(" - time machine finished") + + # list the state machines which shows their current state + for state_machine in self.state_machines: + TNetwork2._debug(" - machine: %r", state_machine) + + # each one has a list of sent/received pdus + for direction, pdu in state_machine.transaction_log: + TNetwork2._debug(" %s %s %s", + direction, + pdu.pduSource or pdu.pduDestination, + pdu.__class__.__name__, + ) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork2._debug) + + # check for success + all_success, some_failed = super(TNetwork2, self).check_for_success() + assert all_success + + +@bacpypes_debugging +class TestSimple(unittest.TestCase): + + def test_idle(self): + """Test an idle network, nothing happens is success.""" + if _debug: TestSimple._debug("test_idle") + + # create a network + tnet = TNetwork1() + + # all start states are successful + tnet.td.start_state.success() + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + + +@bacpypes_debugging +class TestNetworkNumberIs(unittest.TestCase): + + def test_1(self): + """Test broadcasts from a router.""" + if _debug: TestNetworkNumberIs._debug("test_1") + + # create a network + tnet = TNetwork1() + + # tell the IUT to send what it knows + deferred(tnet.iut1.nse.network_number_is) + + # TD sees same traffic as sniffer2 + tnet.td.start_state.success() + + # network 1 sees router from 1 to 2 + tnet.sniffer1.start_state.doc("1-1-0") \ + .receive(NetworkNumberIs, + nniNet=1, + nniFlag=1, + ).doc("1-1-1") \ + .success() + + # network 2 sees router from 2 to 1 + tnet.sniffer2.start_state.doc("1-2-0") \ + .receive(NetworkNumberIs, + nniNet=2, + nniFlag=1, + ).doc("1-2-1") \ + .success() + + # network 3 sees nothing, message isn't routed + tnet.sniffer3.start_state.doc("1-3-0") \ + .timeout(10).doc("1-3-1") \ + .success() + + # run the group + tnet.run() + + def test_2(self): + """Test router response to queries.""" + if _debug: TestNetworkNumberIs._debug("test_2") + + # create a network + tnet = TNetwork1() + + # test device broadcasts a request for the network number + s211 = tnet.td.start_state.doc("2-1-0") \ + .send(WhatIsNetworkNumber( + destination=LocalBroadcast(), + )).doc("2-1-1") \ + + # test device sees both responses + both = s211 \ + .receive(NetworkNumberIs, + pduSource=Address(5), + nniNet=2, + nniFlag=1, + ).doc("2-1-2-a") \ + .receive(NetworkNumberIs, + pduSource=Address(6), + nniNet=2, + nniFlag=1, + ).doc("2-1-2-b") \ + + # allow the two packets in either order + s211.receive(NetworkNumberIs, + pduSource=Address(6), + nniNet=2, + nniFlag=1, + ).doc("2-1-2-c") \ + .receive(NetworkNumberIs, + pduSource=Address(5), + nniNet=2, + nniFlag=1, + next_state=both, + ).doc("2-1-2-d") \ + + # fail if anything is received after both packets + both.timeout(3).doc("2-1-3") \ + .success() + + # short circuit sniffers + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + + def test_3(self): + """Test broadcasts from a router.""" + if _debug: TestNetworkNumberIs._debug("test_3") + + # create a network + tnet = TNetwork2() + + # tell the IUT to send what it knows + deferred(tnet.iut1.nse.network_number_is) + + # TD sees same traffic as sniffer2 + tnet.td.start_state.success() + + # network 1 sees router from 1 to 2 + tnet.sniffer1.start_state.doc("3-1-0") \ + .receive(NetworkNumberIs, + nniNet=1, + nniFlag=1, + ).doc("3-1-1") \ + .success() + + # network 2 sees nothing + tnet.sniffer2.start_state.doc("3-2-0") \ + .timeout(10).doc("3-2-1") \ + .success() + + # network 3 sees nothing + tnet.sniffer3.start_state.doc("3-3-0") \ + .timeout(10).doc("3-3-1") \ + .success() + + # run the group + tnet.run() + + def test_4(self): + """Test router response to queries.""" + if _debug: TestNetworkNumberIs._debug("test_4") + + # create a network + tnet = TNetwork2() + + def iut1_knows_net(net): + assert net in tnet.iut1.nsap.adapters + + # test device sends request, receives one response + tnet.td.start_state.doc("4-1-0") \ + .call(iut1_knows_net, None).doc("4-1-1") \ + .send(WhatIsNetworkNumber( + destination=LocalBroadcast(), + )).doc("4-1-2") \ + .receive(NetworkNumberIs, + pduSource=Address(6), + nniNet=2, + nniFlag=1, + ).doc("4-1-3") \ + .timeout(3).doc("4-1-4") \ + .call(iut1_knows_net, 2).doc("4-1-5") \ + .success() + + # short circuit sniffers + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + + def test_5(self): + """Test router response to queries.""" + if _debug: TestNetworkNumberIs._debug("test_5") + + # create a network + tnet = TNetwork2() + + # tell the IUT2 to send what it knows + deferred(tnet.iut2.nse.network_number_is) + + # test device receives announcement from IUT2, requests network + # number from IUT1, receives announcement from IUT1 with the + # network learned + tnet.td.start_state.doc("5-1-0") \ + .receive(NetworkNumberIs, + pduSource=Address(6), + nniNet=2, + nniFlag=1, + ).doc("5-1-1") \ + .send(WhatIsNetworkNumber( + destination=Address(5), + )).doc("5-1-2") \ + .receive(NetworkNumberIs, + pduSource=Address(5), + nniNet=2, + nniFlag=0, + ).doc("5-1-3") \ + .timeout(3).doc("5-1-4") \ + .success() + + # short circuit sniffers + tnet.sniffer1.start_state.success() + tnet.sniffer2.start_state.success() + tnet.sniffer3.start_state.success() + + # run the group + tnet.run() + diff --git a/tests/test_primitive_data/test_object_identifier.py b/tests/test_primitive_data/test_object_identifier.py index c97a1444..6707ef74 100644 --- a/tests/test_primitive_data/test_object_identifier.py +++ b/tests/test_primitive_data/test_object_identifier.py @@ -78,8 +78,6 @@ def test_object_identifier(self): obj = ObjectIdentifier() assert obj.value == ('analogInput', 0) - with self.assertRaises(TypeError): - ObjectIdentifier("some string") with self.assertRaises(TypeError): ObjectIdentifier(1.0) @@ -94,6 +92,26 @@ def test_object_identifier_int(self): assert obj.value == ('analogOutput', 2) assert str(obj) == "ObjectIdentifier(analogOutput,2)" + def test_object_identifier_str(self): + if _debug: TestObjectIdentifier._debug("test_object_identifier_str") + + obj = ObjectIdentifier("analogInput:1") + assert obj.value == ('analogInput', 1) + assert str(obj) == "ObjectIdentifier(analogInput,1)" + + obj = ObjectIdentifier("8:123") + assert obj.value == ('device', 123) + assert str(obj) == "ObjectIdentifier(device,123)" + + with self.assertRaises(ValueError): + ObjectIdentifier("x") + with self.assertRaises(ValueError): + ObjectIdentifier(":1") + with self.assertRaises(ValueError): + ObjectIdentifier("1:") + with self.assertRaises(ValueError): + ObjectIdentifier("a:b") + def test_object_identifier_tuple(self): if _debug: TestObjectIdentifier._debug("test_object_identifier_tuple") diff --git a/tests/test_service/__init__.py b/tests/test_service/__init__.py index 4e65115d..6c607e51 100644 --- a/tests/test_service/__init__.py +++ b/tests/test_service/__init__.py @@ -5,6 +5,10 @@ """ from . import test_cov +from . import test_cov_av +from . import test_cov_bv +from . import test_cov_pc + from . import test_device from . import test_file from . import test_object diff --git a/tests/test_service/helpers.py b/tests/test_service/helpers.py index c8a6fc1b..5565df45 100644 --- a/tests/test_service/helpers.py +++ b/tests/test_service/helpers.py @@ -5,11 +5,12 @@ """ from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.capability import Capability from bacpypes.comm import Client, bind from bacpypes.pdu import Address, LocalBroadcast from bacpypes.npdu import NPDU -from bacpypes.apdu import APDU, apdu_types +from bacpypes.apdu import apdu_types, APDU, SimpleAckPDU, RejectPDU, AbortPDU from bacpypes.vlan import Network, Node @@ -302,3 +303,40 @@ def confirmation(self, apdu): # allow the application to process it super(ApplicationStateMachine, self).confirmation(apdu) + +# +# COVTestClientServices +# + +@bacpypes_debugging +class COVTestClientServices(Capability): + + def do_ConfirmedCOVNotificationRequest(self, apdu): + if _debug: COVTestClientServices._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + + # the test device needs to set these + assert hasattr(self, 'test_ack') + assert hasattr(self, 'test_reject') + assert hasattr(self, 'test_abort') + + if self.test_ack: + # success + response = SimpleAckPDU(context=apdu) + if _debug: COVTestClientServices._debug(" - simple_ack: %r", response) + + elif self.test_reject: + # reject + response = RejectPDU(reason=self.test_reject, context=apdu) + if _debug: COVTestClientServices._debug(" - reject: %r", response) + + elif self.test_abort: + # abort + response = AbortPDU(reason=self.test_abort, context=apdu) + if _debug: COVTestClientServices._debug(" - abort: %r", response) + + # return the result + self.response(response) + + def do_UnconfirmedCOVNotificationRequest(self, apdu): + if _debug: COVTestClientServices._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + diff --git a/tests/test_service/test_cov.py b/tests/test_service/test_cov.py index 5aae576d..3519e03e 100644 --- a/tests/test_service/test_cov.py +++ b/tests/test_service/test_cov.py @@ -2,83 +2,22 @@ # -*- coding: utf-8 -*- """ -Test Device Services --------------------- +Test COV Service +---------------- """ import unittest from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.capability import Capability +from bacpypes.service.cov import ChangeOfValueServices -from bacpypes.pdu import Address, PDU -from bacpypes.basetypes import ( - DeviceAddress, COVSubscription, PropertyValue, - Recipient, RecipientProcess, ObjectPropertyReference, - ) -from bacpypes.apdu import ( - SubscribeCOVRequest, - ReadPropertyRequest, ReadPropertyACK, - ConfirmedCOVNotificationRequest, UnconfirmedCOVNotificationRequest, - SimpleAckPDU, Error, RejectPDU, AbortPDU, - ) - -from bacpypes.service.object import ( - ReadWritePropertyServices, - ) - -from bacpypes.service.cov import ( - ChangeOfValueServices, - ) -from bacpypes.local.device import LocalDeviceObject -from bacpypes.object import ( - BinaryValueObject, - ) - -from .helpers import ApplicationNetwork, ApplicationStateMachine +from .helpers import ApplicationNetwork # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# COVTestClientServices -# - -@bacpypes_debugging -class COVTestClientServices(Capability): - - def do_ConfirmedCOVNotificationRequest(self, apdu): - if _debug: COVTestClientServices._debug("do_ConfirmedCOVNotificationRequest %r", apdu) - - # the test device needs to set these - assert hasattr(self, 'test_ack') - assert hasattr(self, 'test_reject') - assert hasattr(self, 'test_abort') - - if self.test_ack: - # success - response = SimpleAckPDU(context=apdu) - if _debug: COVTestClientServices._debug(" - simple_ack: %r", response) - - elif self.test_reject: - # reject - response = RejectPDU(reason=self.test_reject, context=apdu) - if _debug: COVTestClientServices._debug(" - reject: %r", response) - - elif self.test_abort: - # abort - response = AbortPDU(reason=self.test_abort, context=apdu) - if _debug: COVTestClientServices._debug(" - abort: %r", response) - - # return the result - self.response(response) - - def do_UnconfirmedCOVNotificationRequest(self, apdu): - if _debug: COVTestClientServices._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) - - @bacpypes_debugging class TestBasic(unittest.TestCase): @@ -99,654 +38,3 @@ def test_basic(self): # run the group anet.run() - -@bacpypes_debugging -class TestBinaryValue(unittest.TestCase): - - def test_8_10_1(self): - """Confirmed Notifications Subscription""" - if _debug: TestBinaryValue._debug("test_8_10_1") - - # create a network - anet = ApplicationNetwork("test_8_10_1") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - anet.iut.add_capability(ReadWritePropertyServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # wait for the subscription - anet.iut.start_state.doc("8.10.1-1-0") \ - .receive(SubscribeCOVRequest).doc("8.10.1-1-1") \ - .success() - - # send the subscription, wait for the ack - anet.td.start_state.doc("8.10.1-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=True, - lifetime=30, - )).doc("8.10.1-2-1") \ - .receive(SimpleAckPDU).doc("8.10.1-2-2") \ - .success() - - # run the group - anet.run() - - def test_8_10_2(self): - """Unconfirmed Notifications Subscription""" - if _debug: TestBinaryValue._debug("test_8_10_2") - - # create a network - anet = ApplicationNetwork("test_8_10_2") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # wait for the subscription - anet.iut.start_state.doc("8.10.2-1-0") \ - .receive(SubscribeCOVRequest).doc("8.10.2-1-1") \ - .success() - - # send the subscription, wait for the ack - anet.td.start_state.doc("8.10.2-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("8.10.2-2-1") \ - .receive(SimpleAckPDU).doc("8.10.2-2-2") \ - .success() - - # run the group, cut the time limit short - anet.run(time_limit=5.0) - - # check that the IUT still has the detection - if _debug: TestBinaryValue._debug(" - detections: %r", anet.iut.cov_detections) - assert len(anet.iut.cov_detections) == 1 - - # pop out the subscription list and criteria - obj_ref, criteria = anet.iut.cov_detections.popitem() - if _debug: TestBinaryValue._debug(" - criteria: %r", criteria) - - # get the list of subscriptions from the criteria - subscriptions = criteria.cov_subscriptions.cov_subscriptions - if _debug: TestBinaryValue._debug(" - subscriptions: %r", subscriptions) - assert len(subscriptions) == 1 - - def test_8_10_3(self): - """Canceling a Subscription""" - if _debug: TestBinaryValue._debug("test_8_10_3") - - # create a network - anet = ApplicationNetwork("test_8_10_3") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # wait for the subscription, then for the cancelation - anet.iut.start_state.doc("8.10.3-1-0") \ - .receive(SubscribeCOVRequest).doc("8.10.3-1-1") \ - .receive(SubscribeCOVRequest).doc("8.10.3-1-2") \ - .success() - - # send the subscription, wait for the ack, then send the cancelation - # and wait for the ack. Ignore the notification that is sent when - # after the subscription - subscription_acked = anet.td.start_state.doc("8.10.3-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("8.10.3-2-1") \ - .ignore(UnconfirmedCOVNotificationRequest) \ - .receive(SimpleAckPDU).doc("8.10.3-2-2") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - )).doc("8.10.3-2-1") \ - .ignore(UnconfirmedCOVNotificationRequest) \ - .receive(SimpleAckPDU).doc("8.10.3-2-2") \ - .success() - - # run the group - anet.run() - - def test_8_10_4(self): - """Requests 8 Hour Lifetimes""" - if _debug: TestBinaryValue._debug("test_8_10_4") - - # create a network - anet = ApplicationNetwork("test_8_10_4") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # wait for the subscription - anet.iut.start_state.doc("8.10.4-1-0") \ - .receive(SubscribeCOVRequest).doc("8.10.4-1-1") \ - .success() - - # send the subscription, wait for the ack - anet.td.start_state.doc("8.10.4-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=True, - lifetime=28800, - )).doc("8.10.4-2-1") \ - .receive(SimpleAckPDU).doc("8.10.4-2-2") \ - .success() - - # run the group - anet.run() - - def test_9_10_1_1(self): - if _debug: TestBinaryValue._debug("test_9_10_1_1") - - notification_fail_time = 0.5 - - # create a network - anet = ApplicationNetwork("test_9_10_1_1") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # wait for the subscription, wait for the notification ack - anet.iut.start_state.doc("9.10.1.1-1-0") \ - .receive(SubscribeCOVRequest).doc("9.10.1.1-1-1") \ - .receive(SimpleAckPDU).doc("9.10.1.1-1-2") \ - .timeout(10).doc("9.10.1.1-1-3") \ - .success() - - # test device is quiet - wait_for_notification = \ - anet.td.start_state.doc("9.10.1.1-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=True, - lifetime=30, - )).doc("9.10.1.1-2-1") \ - .receive(SimpleAckPDU).doc("9.10.1.1-2-2") - - # after the ack, don't wait too long for the notification - wait_for_notification \ - .timeout(notification_fail_time).doc("9.10.1.1-2-3").fail() - - # if the notification is received, success - wait_for_notification \ - .receive(ConfirmedCOVNotificationRequest).doc("9.10.1.1-2-4") \ - .timeout(10).doc("9.10.1.1-2-5") \ - .success() - - # run the group - anet.run() - - def test_no_traffic(self): - """Test basic configuration of a network.""" - if _debug: TestBinaryValue._debug("test_no_traffic") - - # create a network - anet = ApplicationNetwork("test_no_traffic") - - # add the service capability to the IUT - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # make some transitions - anet.iut.start_state.doc("1-1-0") \ - .call(write_test_bv, 'active').doc("1-1-1") \ - .timeout(1).doc("1-1-2") \ - .call(write_test_bv, 'inactive').doc("1-1-3") \ - .timeout(1).doc("1-1-4") \ - .success() - - # test device is quiet - anet.td.start_state.timeout(5).success() - - # run the group - anet.run() - - def test_simple_transition_confirmed(self): - if _debug: TestBinaryValue._debug("test_simple_transition_confirmed") - - # create a network - anet = ApplicationNetwork("test_simple_transition_confirmed") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # receive the subscription request, wait until the client has - # received the ack and the 'instant' notification. Then change the - # value and wait for the ack. - anet.iut.start_state.doc("2-1-0") \ - .receive(SubscribeCOVRequest).doc("2-1-1") \ - .receive(SimpleAckPDU).doc("2-1-2") \ - .wait_event("e1").doc("2-1-3") \ - .call(write_test_bv, 'active').doc("2-1-4") \ - .receive(SimpleAckPDU).doc("2-1-5") \ - .timeout(10).doc("2-2-6") \ - .success() - - # send the subscription request, wait for the ack and the 'instant' - # notification, set the event so the IUT can continue, then wait - # for the next notification - anet.td.start_state.doc("2-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=True, - lifetime=30, - )).doc("2-2-1") \ - .receive(SimpleAckPDU).doc("2-2-2") \ - .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ - .set_event("e1").doc("2-2-3") \ - .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ - .timeout(10).doc("2-2-5") \ - .success() - - # run the group - anet.run() - - def test_simple_transition_unconfirmed(self): - if _debug: TestBinaryValue._debug("test_simple_transition_unconfirmed") - - # create a network - anet = ApplicationNetwork("test_simple_transition_unconfirmed") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) - - # add it to the implementation - anet.iut.add_object(test_bv) - - # tell the TD how to respond to confirmed notifications - anet.td.test_ack = True - anet.td.test_reject = None - anet.td.test_abort = None - - # receive the subscription request, wait until the client has - # received the ack and the 'instant' notification. Then change the - # value, no ack coming back - anet.iut.start_state.doc("3-1-0") \ - .receive(SubscribeCOVRequest).doc("3-1-1") \ - .wait_event("e1").doc("3-1-2") \ - .call(write_test_bv, 'active').doc("3-1-3") \ - .timeout(10).doc("3-2-4") \ - .success() - - # test device is quiet - anet.td.start_state.doc("3-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("3-2-1") \ - .receive(SimpleAckPDU).doc("3-2-2") \ - .receive(UnconfirmedCOVNotificationRequest).doc("3-2-3") \ - .set_event("e1").doc("3-2-4") \ - .receive(UnconfirmedCOVNotificationRequest).doc("3-2-5") \ - .timeout(10).doc("3-2-6") \ - .success() - - # run the group - anet.run() - - def test_changing_status_flags(self): - """This test changes the status flags of binary value point to verify - that the detection picks up other changes, most tests just change the - present value.""" - if _debug: TestBinaryValue._debug("test_changing_status_flags") - - # create a network - anet = ApplicationNetwork("test_changing_status_flags") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - def test_bv_fault(): - if _debug: TestBinaryValue._debug("test_bv_fault") - test_bv.statusFlags = [0, 1, 0, 0] - - # add it to the implementation - anet.iut.add_object(test_bv) - - # receive the subscription request, wait until the client has - # received the ack and the 'instant' notification. Then change the - # value, no ack coming back - anet.iut.start_state.doc("4-1-0") \ - .receive(SubscribeCOVRequest).doc("4-1-1") \ - .wait_event("e1").doc("4-1-2") \ - .call(test_bv_fault).doc("4-1-3") \ - .timeout(10).doc("4-2-4") \ - .success() - - # test device is quiet - anet.td.start_state.doc("4-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("4-2-1") \ - .receive(SimpleAckPDU).doc("4-2-2") \ - .receive(UnconfirmedCOVNotificationRequest).doc("4-2-3") \ - .set_event("e1").doc("4-2-4") \ - .receive(UnconfirmedCOVNotificationRequest).doc("4-2-5") \ - .timeout(10).doc("4-2-6") \ - .success() - - # run the group - anet.run() - - def test_changing_properties(self): - """This test changes the value of multiple properties to verify that - only one COV notification is sent.""" - if _debug: TestBinaryValue._debug("test_changing_properties") - - # create a network - anet = ApplicationNetwork("test_changing_properties") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change the present value - def test_bv_fault(): - if _debug: TestBinaryValue._debug("test_bv_fault") - test_bv.presentValue = 'active' - test_bv.statusFlags = [0, 0, 1, 0] - - # add it to the implementation - anet.iut.add_object(test_bv) - - # receive the subscription request, wait until the client has - # received the ack and the 'instant' notification. Then change the - # value, no ack coming back - anet.iut.start_state.doc("5-1-0") \ - .receive(SubscribeCOVRequest).doc("5-1-1") \ - .wait_event("e1").doc("5-1-2") \ - .call(test_bv_fault).doc("5-1-3") \ - .timeout(10).doc("5-2-4") \ - .success() - - # test device is quiet - anet.td.start_state.doc("5-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("5-2-1") \ - .receive(SimpleAckPDU).doc("5-2-2") \ - .receive(UnconfirmedCOVNotificationRequest).doc("5-2-3") \ - .set_event("e1").doc("5-2-4") \ - .receive(UnconfirmedCOVNotificationRequest).doc("5-2-5") \ - .timeout(10).doc("5-2-6") \ - .success() - - # run the group - anet.run() - - def test_multiple_subscribers(self): - """This has more than one subscriber for the object.""" - if _debug: TestBinaryValue._debug("test_multiple_subscribers") - - # create a network - anet = ApplicationNetwork("test_multiple_subscribers") - - # add the service capability to the IUT - anet.td.add_capability(COVTestClientServices) - anet.iut.add_capability(ChangeOfValueServices) - - # make a binary value object - test_bv = BinaryValueObject( - objectIdentifier=('binaryValue', 1), - objectName='bv', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - - # an easy way to change both the present value and status flags - # which should trigger only one notification - def test_bv_fault(): - if _debug: TestBinaryValue._debug("test_bv_fault") - test_bv.presentValue = 'active' - test_bv.statusFlags = [0, 0, 1, 0] - - # add it to the implementation - anet.iut.add_object(test_bv) - - # add another test device object - anet.td2_device_object = LocalDeviceObject( - objectName="td2", - objectIdentifier=("device", 30), - maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', - vendorIdentifier=999, - ) - - # another test device - anet.td2 = ApplicationStateMachine(anet.td2_device_object, anet.vlan) - anet.td2.add_capability(COVTestClientServices) - anet.append(anet.td2) - - # receive the subscription requests, wait until both clients have - # received the ack and the 'instant' notification. Then change the - # value, no ack coming back - anet.iut.start_state.doc("6-1-0") \ - .receive(SubscribeCOVRequest, pduSource=anet.td.address).doc("6-1-1") \ - .receive(SubscribeCOVRequest, pduSource=anet.td2.address).doc("6-1-2") \ - .wait_event("e2").doc("6-1-3") \ - .call(test_bv_fault).doc("6-1-4") \ - .timeout(10).doc("6-2-5") \ - .success() - - # first test device; send the subscription request, get an ack - # followed by the 'instant' notification - anet.td.start_state.doc("6-2-0") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("6-2-1") \ - .receive(SimpleAckPDU).doc("6-2-2") \ - .receive(UnconfirmedCOVNotificationRequest).doc("6-2-3") \ - .set_event("e1").doc("6-2-4") \ - .receive(UnconfirmedCOVNotificationRequest).doc("6-2-5") \ - .timeout(10).doc("6-2-6") \ - .success() - - # same pa - anet.td2.start_state.doc("6-3-0") \ - .wait_event("e1").doc("6-3-1") \ - .send(SubscribeCOVRequest( - destination=anet.iut.address, - subscriberProcessIdentifier=1, - monitoredObjectIdentifier=('binaryValue', 1), - issueConfirmedNotifications=False, - lifetime=30, - )).doc("6-3-2") \ - .receive(SimpleAckPDU).doc("6-3-3") \ - .receive(UnconfirmedCOVNotificationRequest).doc("6-3-4") \ - .set_event("e2").doc("6-3-5") \ - .receive(UnconfirmedCOVNotificationRequest).doc("6-3-6") \ - .timeout(10).doc("6-3-7") \ - .success() - - # run the group - anet.run() - diff --git a/tests/test_service/test_cov_av.py b/tests/test_service/test_cov_av.py new file mode 100644 index 00000000..b745eb35 --- /dev/null +++ b/tests/test_service/test_cov_av.py @@ -0,0 +1,720 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Analog Value COV Services +------------------------------ +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.apdu import ( + SubscribeCOVRequest, SimpleAckPDU, + ConfirmedCOVNotificationRequest, UnconfirmedCOVNotificationRequest, + ) + +from bacpypes.service.cov import ChangeOfValueServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.object import AnalogValueObject + +from .helpers import ApplicationNetwork, ApplicationStateMachine, COVTestClientServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestAnalogValue(unittest.TestCase): + + def test_8_10_1(self): + """Confirmed Notifications Subscription""" + if _debug: TestAnalogValue._debug("test_8_10_1") + + # create a network + anet = ApplicationNetwork("test_8_10_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # add it to the implementation + anet.iut.add_object(test_av) + + # wait for the subscription + anet.iut.start_state.doc("8.10.1-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.1-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("8.10.1-2-1") \ + .receive(SimpleAckPDU).doc("8.10.1-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_2(self): + """Unconfirmed Notifications Subscription""" + if _debug: TestAnalogValue._debug("test_8_10_2") + + # create a network + anet = ApplicationNetwork("test_8_10_2") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # add it to the implementation + anet.iut.add_object(test_av) + + # wait for the subscription + anet.iut.start_state.doc("8.10.2-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.2-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.2-2-1") \ + .receive(SimpleAckPDU).doc("8.10.2-2-2") \ + .success() + + # run the group, cut the time limit short + anet.run(time_limit=5.0) + + # check that the IUT still has the detection + if _debug: TestAnalogValue._debug(" - detections: %r", anet.iut.cov_detections) + assert len(anet.iut.cov_detections) == 1 + + # pop out the subscription list and criteria + obj_ref, criteria = anet.iut.cov_detections.popitem() + if _debug: TestAnalogValue._debug(" - criteria: %r", criteria) + + # get the list of subscriptions from the criteria + subscriptions = criteria.cov_subscriptions.cov_subscriptions + if _debug: TestAnalogValue._debug(" - subscriptions: %r", subscriptions) + assert len(subscriptions) == 1 + + def test_8_10_3(self): + """Canceling a Subscription""" + if _debug: TestAnalogValue._debug("test_8_10_3") + + # create a network + anet = ApplicationNetwork("test_8_10_3") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # add it to the implementation + anet.iut.add_object(test_av) + + # wait for the subscription, then for the cancelation + anet.iut.start_state.doc("8.10.3-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-1") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-2") \ + .success() + + # send the subscription, wait for the ack, then send the cancelation + # and wait for the ack. Ignore the notification that is sent when + # after the subscription + anet.td.start_state.doc("8.10.3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_4(self): + """Requests 8 Hour Lifetimes""" + if _debug: TestAnalogValue._debug("test_8_10_4") + + # create a network + anet = ApplicationNetwork("test_8_10_4") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # add it to the implementation + anet.iut.add_object(test_av) + + # wait for the subscription + anet.iut.start_state.doc("8.10.4-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.4-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=True, + lifetime=28800, + )).doc("8.10.4-2-1") \ + .receive(SimpleAckPDU).doc("8.10.4-2-2") \ + .success() + + # run the group + anet.run() + + def test_9_10_1_1(self): + if _debug: TestAnalogValue._debug("test_9_10_1_1") + + notification_fail_time = 0.5 + + # create a network + anet = ApplicationNetwork("test_9_10_1_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # add it to the implementation + anet.iut.add_object(test_av) + + # wait for the subscription, wait for the notification ack + anet.iut.start_state.doc("9.10.1.1-1-0") \ + .receive(SubscribeCOVRequest).doc("9.10.1.1-1-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-1-2") \ + .timeout(10).doc("9.10.1.1-1-3") \ + .success() + + # test device is quiet + wait_for_notification = \ + anet.td.start_state.doc("9.10.1.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("9.10.1.1-2-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-2-2") + + # after the ack, don't wait too long for the notification + wait_for_notification \ + .timeout(notification_fail_time).doc("9.10.1.1-2-3").fail() + + # if the notification is received, success + wait_for_notification \ + .receive(ConfirmedCOVNotificationRequest).doc("9.10.1.1-2-4") \ + .timeout(10).doc("9.10.1.1-2-5") \ + .success() + + # run the group + anet.run() + + def test_no_traffic(self): + """Test basic configuration of a network.""" + if _debug: TestAnalogValue._debug("test_no_traffic") + + # create a network + anet = ApplicationNetwork("test_no_traffic") + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change the present value + write_test_av = lambda v: setattr(test_av, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_av) + + # make some transitions + anet.iut.start_state.doc("1-1-0") \ + .call(write_test_av, 100.0).doc("1-1-1") \ + .timeout(1).doc("1-1-2") \ + .call(write_test_av, 0.0).doc("1-1-3") \ + .timeout(1).doc("1-1-4") \ + .success() + + # test device is quiet + anet.td.start_state.timeout(5).success() + + # run the group + anet.run() + + def test_8_2_1(self): + """To verify that the IUT can initiate ConfirmedCOVNotification service + requests conveying a change of the Present_Value property of Analog + Input, Analog Output, and Analog Value objects.""" + if _debug: TestAnalogValue._debug("test_8_2_1") + + # create a network + anet = ApplicationNetwork("test_8_2_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change the present value + write_test_av = lambda v: setattr(test_av, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_av) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value a little bit and nothing should be sent. Change it some more + # and wait for the notification ack. + anet.iut.start_state.doc("2-1-0") \ + .receive(SubscribeCOVRequest).doc("2-1-1") \ + .receive(SimpleAckPDU).doc("2-1-2") \ + .wait_event("e1").doc("2-1-3") \ + .call(write_test_av, 5.0).doc("2-1-4") \ + .timeout(5).doc("2-2-5") \ + .call(write_test_av, 10.0).doc("2-1-6") \ + .receive(SimpleAckPDU).doc("2-1-7") \ + .timeout(10).doc("2-2-8") \ + .success() + + # send the subscription request, wait for the ack and the 'instant' + # notification, set the event so the IUT can continue, then wait + # for the next notification + anet.td.start_state.doc("2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("2-2-1") \ + .receive(SimpleAckPDU).doc("2-2-2") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ + .set_event("e1").doc("2-2-3") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ + .timeout(10).doc("2-2-5") \ + .success() + + # run the group + anet.run() + + def test_simple_transition_unconfirmed(self): + if _debug: TestAnalogValue._debug("test_simple_transition_unconfirmed") + + # create a network + anet = ApplicationNetwork("test_simple_transition_unconfirmed") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change the present value + write_test_av = lambda v: setattr(test_av, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_av) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("3-1-0") \ + .receive(SubscribeCOVRequest).doc("3-1-1") \ + .wait_event("e1").doc("3-1-2") \ + .call(write_test_av, 100.0).doc("3-1-3") \ + .timeout(10).doc("3-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("3-2-1") \ + .receive(SimpleAckPDU).doc("3-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-3") \ + .set_event("e1").doc("3-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-5") \ + .timeout(10).doc("3-2-6") \ + .success() + + # run the group + anet.run() + + def test_changing_status_flags(self): + """This test changes the status flags of binary value point to verify + that the detection picks up other changes, most tests just change the + present value.""" + if _debug: TestAnalogValue._debug("test_changing_status_flags") + + # create a network + anet = ApplicationNetwork("test_changing_status_flags") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change the present value + def test_av_fault(): + if _debug: TestAnalogValue._debug("test_av_fault") + test_av.statusFlags = [0, 1, 0, 0] + + # add it to the implementation + anet.iut.add_object(test_av) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("4-1-0") \ + .receive(SubscribeCOVRequest).doc("4-1-1") \ + .wait_event("e1").doc("4-1-2") \ + .call(test_av_fault).doc("4-1-3") \ + .timeout(10).doc("4-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("4-2-1") \ + .receive(SimpleAckPDU).doc("4-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-3") \ + .set_event("e1").doc("4-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-5") \ + .timeout(10).doc("4-2-6") \ + .success() + + # run the group + anet.run() + + def test_changing_properties(self): + """This test changes the value of multiple properties to verify that + only one COV notification is sent.""" + if _debug: TestAnalogValue._debug("test_changing_properties") + + # create a network + anet = ApplicationNetwork("test_changing_properties") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change the present value + def test_av_fault(): + if _debug: TestAnalogValue._debug("test_av_fault") + test_av.presentValue = 100.0 + test_av.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_av) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("5-1-0") \ + .receive(SubscribeCOVRequest).doc("5-1-1") \ + .wait_event("e1").doc("5-1-2") \ + .call(test_av_fault).doc("5-1-3") \ + .timeout(10).doc("5-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("5-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("5-2-1") \ + .receive(SimpleAckPDU).doc("5-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-3") \ + .set_event("e1").doc("5-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-5") \ + .timeout(10).doc("5-2-6") \ + .success() + + # run the group + anet.run() + + def test_multiple_subscribers(self): + """This has more than one subscriber for the object.""" + if _debug: TestAnalogValue._debug("test_multiple_subscribers") + + # create a network + anet = ApplicationNetwork("test_multiple_subscribers") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=10.0, + ) + + # an easy way to change both the present value and status flags + # which should trigger only one notification + def test_av_fault(): + if _debug: TestAnalogValue._debug("test_av_fault") + test_av.presentValue = 100.0 + test_av.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_av) + + # add another test device object + anet.td2_device_object = LocalDeviceObject( + objectName="td2", + objectIdentifier=('device', 30), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=999, + ) + + # another test device + anet.td2 = ApplicationStateMachine(anet.td2_device_object, anet.vlan) + anet.td2.add_capability(COVTestClientServices) + anet.append(anet.td2) + + # receive the subscription requests, wait until both clients have + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("6-1-0") \ + .receive(SubscribeCOVRequest, pduSource=anet.td.address).doc("6-1-1") \ + .receive(SubscribeCOVRequest, pduSource=anet.td2.address).doc("6-1-2") \ + .wait_event("e2").doc("6-1-3") \ + .call(test_av_fault).doc("6-1-4") \ + .timeout(10).doc("6-2-5") \ + .success() + + # first test device; send the subscription request, get an ack + # followed by the 'instant' notification + anet.td.start_state.doc("6-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-2-1") \ + .receive(SimpleAckPDU).doc("6-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-3") \ + .set_event("e1").doc("6-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-5") \ + .timeout(10).doc("6-2-6") \ + .success() + + # same pattern for the other test device + anet.td2.start_state.doc("6-3-0") \ + .wait_event("e1").doc("6-3-1") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('analogValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-3-2") \ + .receive(SimpleAckPDU).doc("6-3-3") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-4") \ + .set_event("e2").doc("6-3-5") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-6") \ + .timeout(10).doc("6-3-7") \ + .success() + + # run the group + anet.run() + diff --git a/tests/test_service/test_cov_bv.py b/tests/test_service/test_cov_bv.py new file mode 100644 index 00000000..e30fe7ce --- /dev/null +++ b/tests/test_service/test_cov_bv.py @@ -0,0 +1,709 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Binary Value COV Services +------------------------------ +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.apdu import ( + SubscribeCOVRequest, SimpleAckPDU, + ConfirmedCOVNotificationRequest, UnconfirmedCOVNotificationRequest, + ) + +from bacpypes.service.cov import ChangeOfValueServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.object import BinaryValueObject + +from .helpers import ApplicationNetwork, ApplicationStateMachine, COVTestClientServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestBinaryValue(unittest.TestCase): + + def test_8_10_1(self): + """Confirmed Notifications Subscription""" + if _debug: TestBinaryValue._debug("test_8_10_1") + + # create a network + anet = ApplicationNetwork("test_8_10_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # wait for the subscription + anet.iut.start_state.doc("8.10.1-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.1-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("8.10.1-2-1") \ + .receive(SimpleAckPDU).doc("8.10.1-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_2(self): + """Unconfirmed Notifications Subscription""" + if _debug: TestBinaryValue._debug("test_8_10_2") + + # create a network + anet = ApplicationNetwork("test_8_10_2") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # wait for the subscription + anet.iut.start_state.doc("8.10.2-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.2-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.2-2-1") \ + .receive(SimpleAckPDU).doc("8.10.2-2-2") \ + .success() + + # run the group, cut the time limit short + anet.run(time_limit=5.0) + + # check that the IUT still has the detection + if _debug: TestBinaryValue._debug(" - detections: %r", anet.iut.cov_detections) + assert len(anet.iut.cov_detections) == 1 + + # pop out the subscription list and criteria + obj_ref, criteria = anet.iut.cov_detections.popitem() + if _debug: TestBinaryValue._debug(" - criteria: %r", criteria) + + # get the list of subscriptions from the criteria + subscriptions = criteria.cov_subscriptions.cov_subscriptions + if _debug: TestBinaryValue._debug(" - subscriptions: %r", subscriptions) + assert len(subscriptions) == 1 + + def test_8_10_3(self): + """Canceling a Subscription""" + if _debug: TestBinaryValue._debug("test_8_10_3") + + # create a network + anet = ApplicationNetwork("test_8_10_3") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # wait for the subscription, then for the cancelation + anet.iut.start_state.doc("8.10.3-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-1") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-2") \ + .success() + + # send the subscription, wait for the ack, then send the cancelation + # and wait for the ack. Ignore the notification that is sent when + # after the subscription + anet.td.start_state.doc("8.10.3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_4(self): + """Requests 8 Hour Lifetimes""" + if _debug: TestBinaryValue._debug("test_8_10_4") + + # create a network + anet = ApplicationNetwork("test_8_10_4") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # wait for the subscription + anet.iut.start_state.doc("8.10.4-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.4-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=True, + lifetime=28800, + )).doc("8.10.4-2-1") \ + .receive(SimpleAckPDU).doc("8.10.4-2-2") \ + .success() + + # run the group + anet.run() + + def test_9_10_1_1(self): + if _debug: TestBinaryValue._debug("test_9_10_1_1") + + notification_fail_time = 0.5 + + # create a network + anet = ApplicationNetwork("test_9_10_1_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # wait for the subscription, wait for the notification ack + anet.iut.start_state.doc("9.10.1.1-1-0") \ + .receive(SubscribeCOVRequest).doc("9.10.1.1-1-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-1-2") \ + .timeout(10).doc("9.10.1.1-1-3") \ + .success() + + # test device is quiet + wait_for_notification = \ + anet.td.start_state.doc("9.10.1.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("9.10.1.1-2-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-2-2") + + # after the ack, don't wait too long for the notification + wait_for_notification \ + .timeout(notification_fail_time).doc("9.10.1.1-2-3").fail() + + # if the notification is received, success + wait_for_notification \ + .receive(ConfirmedCOVNotificationRequest).doc("9.10.1.1-2-4") \ + .timeout(10).doc("9.10.1.1-2-5") \ + .success() + + # run the group + anet.run() + + def test_no_traffic(self): + """Test basic configuration of a network.""" + if _debug: TestBinaryValue._debug("test_no_traffic") + + # create a network + anet = ApplicationNetwork("test_no_traffic") + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change the present value + write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # make some transitions + anet.iut.start_state.doc("1-1-0") \ + .call(write_test_bv, 'active').doc("1-1-1") \ + .timeout(1).doc("1-1-2") \ + .call(write_test_bv, 'inactive').doc("1-1-3") \ + .timeout(1).doc("1-1-4") \ + .success() + + # test device is quiet + anet.td.start_state.timeout(5).success() + + # run the group + anet.run() + + def test_8_2_3(self): + """To verify that the IUT can initiate ConfirmedCOVNotification + service requests conveying a change of the Present_Value property of + Binary Input, Binary Output, and Binary Value objects.""" + if _debug: TestBinaryValue._debug("test_8_2_3") + + # create a network + anet = ApplicationNetwork("test_simple_transition_confirmed") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change the present value + write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value and wait for the ack. + anet.iut.start_state.doc("2-1-0") \ + .receive(SubscribeCOVRequest).doc("2-1-1") \ + .receive(SimpleAckPDU).doc("2-1-2") \ + .wait_event("e1").doc("2-1-3") \ + .call(write_test_bv, 'active').doc("2-1-4") \ + .receive(SimpleAckPDU).doc("2-1-5") \ + .timeout(10).doc("2-2-6") \ + .success() + + # send the subscription request, wait for the ack and the 'instant' + # notification, set the event so the IUT can continue, then wait + # for the next notification + anet.td.start_state.doc("2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("2-2-1") \ + .receive(SimpleAckPDU).doc("2-2-2") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ + .set_event("e1").doc("2-2-3") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-4") \ + .timeout(10).doc("2-2-5") \ + .success() + + # run the group + anet.run() + + def test_simple_transition_unconfirmed(self): + """To verify that the IUT can initiate UnconfirmedCOVNotification + service requests conveying a change of the Present_Value property of + Binary Input, Binary Output, and Binary Value objects.""" + if _debug: TestBinaryValue._debug("test_simple_transition_unconfirmed") + + # create a network + anet = ApplicationNetwork("test_simple_transition_unconfirmed") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change the present value + write_test_bv = lambda v: setattr(test_bv, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_bv) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("3-1-0") \ + .receive(SubscribeCOVRequest).doc("3-1-1") \ + .wait_event("e1").doc("3-1-2") \ + .call(write_test_bv, 'active').doc("3-1-3") \ + .timeout(10).doc("3-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("3-2-1") \ + .receive(SimpleAckPDU).doc("3-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-3") \ + .set_event("e1").doc("3-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-5") \ + .timeout(10).doc("3-2-6") \ + .success() + + # run the group + anet.run() + + def test_8_2_4(self): + """To verify that the IUT can initiate ConfirmedCOVNotification service + requests conveying a change of the Status_Flags property of Binary + Input, Binary Output, and Binary Value objects.""" + if _debug: TestBinaryValue._debug("test_8_2_4") + + # create a network + anet = ApplicationNetwork("test_changing_status_flags") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change the present value + def test_bv_fault(): + if _debug: TestBinaryValue._debug("test_bv_fault") + test_bv.statusFlags = [0, 1, 0, 0] + + # add it to the implementation + anet.iut.add_object(test_bv) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("4-1-0") \ + .receive(SubscribeCOVRequest).doc("4-1-1") \ + .wait_event("e1").doc("4-1-2") \ + .call(test_bv_fault).doc("4-1-3") \ + .timeout(10).doc("4-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("4-2-1") \ + .receive(SimpleAckPDU).doc("4-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-3") \ + .set_event("e1").doc("4-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-5") \ + .timeout(10).doc("4-2-6") \ + .success() + + # run the group + anet.run() + + def test_changing_properties(self): + """This test changes the value of multiple properties to verify that + only one COV notification is sent.""" + if _debug: TestBinaryValue._debug("test_changing_properties") + + # create a network + anet = ApplicationNetwork("test_changing_properties") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change the present value + def test_bv_fault(): + if _debug: TestBinaryValue._debug("test_bv_fault") + test_bv.presentValue = 'active' + test_bv.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_bv) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("5-1-0") \ + .receive(SubscribeCOVRequest).doc("5-1-1") \ + .wait_event("e1").doc("5-1-2") \ + .call(test_bv_fault).doc("5-1-3") \ + .timeout(10).doc("5-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("5-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("5-2-1") \ + .receive(SimpleAckPDU).doc("5-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-3") \ + .set_event("e1").doc("5-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-5") \ + .timeout(10).doc("5-2-6") \ + .success() + + # run the group + anet.run() + + def test_multiple_subscribers(self): + """This has more than one subscriber for the object.""" + if _debug: TestBinaryValue._debug("test_multiple_subscribers") + + # create a network + anet = ApplicationNetwork("test_multiple_subscribers") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + + # an easy way to change both the present value and status flags + # which should trigger only one notification + def test_bv_fault(): + if _debug: TestBinaryValue._debug("test_bv_fault") + test_bv.presentValue = 'active' + test_bv.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_bv) + + # add another test device object + anet.td2_device_object = LocalDeviceObject( + objectName="td2", + objectIdentifier=('device', 30), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=999, + ) + + # another test device + anet.td2 = ApplicationStateMachine(anet.td2_device_object, anet.vlan) + anet.td2.add_capability(COVTestClientServices) + anet.append(anet.td2) + + # receive the subscription requests, wait until both clients have + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("6-1-0") \ + .receive(SubscribeCOVRequest, pduSource=anet.td.address).doc("6-1-1") \ + .receive(SubscribeCOVRequest, pduSource=anet.td2.address).doc("6-1-2") \ + .wait_event("e2").doc("6-1-3") \ + .call(test_bv_fault).doc("6-1-4") \ + .timeout(10).doc("6-2-5") \ + .success() + + # first test device; send the subscription request, get an ack + # followed by the 'instant' notification + anet.td.start_state.doc("6-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-2-1") \ + .receive(SimpleAckPDU).doc("6-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-3") \ + .set_event("e1").doc("6-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-5") \ + .timeout(10).doc("6-2-6") \ + .success() + + # same pattern for the other test device + anet.td2.start_state.doc("6-3-0") \ + .wait_event("e1").doc("6-3-1") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('binaryValue', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-3-2") \ + .receive(SimpleAckPDU).doc("6-3-3") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-4") \ + .set_event("e2").doc("6-3-5") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-6") \ + .timeout(10).doc("6-3-7") \ + .success() + + # run the group + anet.run() + diff --git a/tests/test_service/test_cov_pc.py b/tests/test_service/test_cov_pc.py new file mode 100644 index 00000000..76e039c5 --- /dev/null +++ b/tests/test_service/test_cov_pc.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Pulse Converter COV Services +--------------------------------- +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.primitivedata import Date, Time +from bacpypes.basetypes import DateTime + +from bacpypes.apdu import ( + SubscribeCOVRequest, SimpleAckPDU, + ConfirmedCOVNotificationRequest, UnconfirmedCOVNotificationRequest, + ) + +from bacpypes.service.cov import ChangeOfValueServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.object import PulseConverterObject + +from .helpers import ApplicationNetwork, ApplicationStateMachine, COVTestClientServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class TestPulseConverter(unittest.TestCase): + + def test_8_10_1(self): + """Confirmed Notifications Subscription""" + if _debug: TestPulseConverter._debug("test_8_10_1") + + # create a network + anet = ApplicationNetwork("test_8_10_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # wait for the subscription + anet.iut.start_state.doc("8.10.1-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.1-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("8.10.1-2-1") \ + .receive(SimpleAckPDU).doc("8.10.1-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_2(self): + """Unconfirmed Notifications Subscription""" + if _debug: TestPulseConverter._debug("test_8_10_2") + + # create a network + anet = ApplicationNetwork("test_8_10_2") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # wait for the subscription + anet.iut.start_state.doc("8.10.2-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.2-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.2-2-1") \ + .receive(SimpleAckPDU).doc("8.10.2-2-2") \ + .success() + + # run the group, cut the time limit short + anet.run(time_limit=5.0) + + # check that the IUT still has the detection + if _debug: TestPulseConverter._debug(" - detections: %r", anet.iut.cov_detections) + assert len(anet.iut.cov_detections) == 1 + + # pop out the subscription list and criteria + obj_ref, criteria = anet.iut.cov_detections.popitem() + if _debug: TestPulseConverter._debug(" - criteria: %r", criteria) + + # get the list of subscriptions from the criteria + subscriptions = criteria.cov_subscriptions.cov_subscriptions + if _debug: TestPulseConverter._debug(" - subscriptions: %r", subscriptions) + assert len(subscriptions) == 1 + + def test_8_10_3(self): + """Canceling a Subscription""" + if _debug: TestPulseConverter._debug("test_8_10_3") + + # create a network + anet = ApplicationNetwork("test_8_10_3") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # wait for the subscription, then for the cancelation + anet.iut.start_state.doc("8.10.3-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-1") \ + .receive(SubscribeCOVRequest).doc("8.10.3-1-2") \ + .success() + + # send the subscription, wait for the ack, then send the cancelation + # and wait for the ack. Ignore the notification that is sent when + # after the subscription + anet.td.start_state.doc("8.10.3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + )).doc("8.10.3-2-1") \ + .ignore(UnconfirmedCOVNotificationRequest) \ + .receive(SimpleAckPDU).doc("8.10.3-2-2") \ + .success() + + # run the group + anet.run() + + def test_8_10_4(self): + """Requests 8 Hour Lifetimes""" + if _debug: TestPulseConverter._debug("test_8_10_4") + + # create a network + anet = ApplicationNetwork("test_8_10_4") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # wait for the subscription + anet.iut.start_state.doc("8.10.4-1-0") \ + .receive(SubscribeCOVRequest).doc("8.10.4-1-1") \ + .success() + + # send the subscription, wait for the ack + anet.td.start_state.doc("8.10.4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=True, + lifetime=28800, + )).doc("8.10.4-2-1") \ + .receive(SimpleAckPDU).doc("8.10.4-2-2") \ + .success() + + # run the group + anet.run() + + def test_9_10_1_1(self): + if _debug: TestPulseConverter._debug("test_9_10_1_1") + + notification_fail_time = 0.5 + + # create a network + anet = ApplicationNetwork("test_9_10_1_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # wait for the subscription, wait for the notification ack + anet.iut.start_state.doc("9.10.1.1-1-0") \ + .receive(SubscribeCOVRequest).doc("9.10.1.1-1-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-1-2") \ + .timeout(10).doc("9.10.1.1-1-3") \ + .success() + + # test device is quiet + wait_for_notification = \ + anet.td.start_state.doc("9.10.1.1-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("9.10.1.1-2-1") \ + .receive(SimpleAckPDU).doc("9.10.1.1-2-2") + + # after the ack, don't wait too long for the notification + wait_for_notification \ + .timeout(notification_fail_time).doc("9.10.1.1-2-3").fail() + + # if the notification is received, success + wait_for_notification \ + .receive(ConfirmedCOVNotificationRequest).doc("9.10.1.1-2-4") \ + .timeout(10).doc("9.10.1.1-2-5") \ + .success() + + # run the group + anet.run() + + def test_no_traffic(self): + """Test basic configuration of a network.""" + if _debug: TestPulseConverter._debug("test_no_traffic") + + # create a network + anet = ApplicationNetwork("test_no_traffic") + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change the present value + write_test_pc = lambda v: setattr(test_pc, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # make some transitions + anet.iut.start_state.doc("1-1-0") \ + .call(write_test_pc, 100.0).doc("1-1-1") \ + .timeout(1).doc("1-1-2") \ + .call(write_test_pc, 0.0).doc("1-1-3") \ + .timeout(1).doc("1-1-4") \ + .success() + + # test device is quiet + anet.td.start_state.timeout(5).success() + + # run the group + anet.run() + + def test_8_2_1(self): + """To verify that the IUT can initiate ConfirmedCOVNotification service + requests conveying a change of the Present_Value property of Analog + Input, Analog Output, and Analog Value objects.""" + if _debug: TestPulseConverter._debug("test_8_2_1") + + # create a network + anet = ApplicationNetwork("test_8_2_1") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change the present value + def write_test_pc(v): + if _debug: TestPulseConverter._debug("=== marco %r", v) + setattr(test_pc, 'presentValue', v) + if _debug: TestPulseConverter._debug("=== polo") + + # add it to the implementation + anet.iut.add_object(test_pc) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value a little bit and nothing should be sent. Change it some more + # and wait for the notification ack. + anet.iut.start_state.doc("2-1-0") \ + .receive(SubscribeCOVRequest).doc("2-1-1") \ + .receive(SimpleAckPDU).doc("2-1-2") \ + .wait_event("e1").doc("2-1-3") \ + .call(write_test_pc, 5.0).doc("2-1-4") \ + .timeout(5).doc("2-1-5") \ + .call(write_test_pc, 10.0).doc("2-1-6") \ + .receive(SimpleAckPDU).doc("2-1-7") \ + .receive(SimpleAckPDU).doc("2-1-8") \ + .timeout(10).doc("2-1-9") \ + .success() + + # send the subscription request, wait for the ack and the 'instant' + # notification, set the event so the IUT can continue, then wait + # for the next notification + anet.td.start_state.doc("2-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=True, + lifetime=30, + )).doc("2-2-1") \ + .receive(SimpleAckPDU).doc("2-2-2") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-3") \ + .set_event("e1").doc("2-2-4") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-5") \ + .receive(ConfirmedCOVNotificationRequest).doc("2-2-6") \ + .timeout(10).doc("2-2-7") \ + .success() + + # run the group + anet.run() + + def test_simple_transition_unconfirmed(self): + if _debug: TestPulseConverter._debug("test_simple_transition_unconfirmed") + + # create a network + anet = ApplicationNetwork("test_simple_transition_unconfirmed") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change the present value + write_test_pc = lambda v: setattr(test_pc, 'presentValue', v) + + # add it to the implementation + anet.iut.add_object(test_pc) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("3-1-0") \ + .receive(SubscribeCOVRequest).doc("3-1-1") \ + .wait_event("e1").doc("3-1-2") \ + .call(write_test_pc, 100.0).doc("3-1-3") \ + .timeout(10).doc("3-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("3-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("3-2-1") \ + .receive(SimpleAckPDU).doc("3-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-3") \ + .set_event("e1").doc("3-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("3-2-5") \ + .timeout(10).doc("3-2-6") \ + .success() + + # run the group + anet.run() + + def test_changing_status_flags(self): + """This test changes the status flags of binary value point to verify + that the detection picks up other changes, most tests just change the + present value.""" + if _debug: TestPulseConverter._debug("test_changing_status_flags") + + # create a network + anet = ApplicationNetwork("test_changing_status_flags") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change the present value + def test_pc_fault(): + if _debug: TestPulseConverter._debug("test_pc_fault") + test_pc.statusFlags = [0, 1, 0, 0] + + # add it to the implementation + anet.iut.add_object(test_pc) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("4-1-0") \ + .receive(SubscribeCOVRequest).doc("4-1-1") \ + .wait_event("e1").doc("4-1-2") \ + .call(test_pc_fault).doc("4-1-3") \ + .timeout(10).doc("4-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("4-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("4-2-1") \ + .receive(SimpleAckPDU).doc("4-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-3") \ + .set_event("e1").doc("4-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("4-2-5") \ + .timeout(10).doc("4-2-6") \ + .success() + + # run the group + anet.run() + + def test_changing_properties(self): + """This test changes the value of multiple properties to verify that + only one COV notification is sent.""" + if _debug: TestPulseConverter._debug("test_changing_properties") + + # create a network + anet = ApplicationNetwork("test_changing_properties") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change the present value + def test_pc_fault(): + if _debug: TestPulseConverter._debug("test_pc_fault") + test_pc.presentValue = 100.0 + test_pc.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_pc) + + # receive the subscription request, wait until the client has + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("5-1-0") \ + .receive(SubscribeCOVRequest).doc("5-1-1") \ + .wait_event("e1").doc("5-1-2") \ + .call(test_pc_fault).doc("5-1-3") \ + .timeout(10).doc("5-2-4") \ + .success() + + # test device is quiet + anet.td.start_state.doc("5-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("5-2-1") \ + .receive(SimpleAckPDU).doc("5-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-3") \ + .set_event("e1").doc("5-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("5-2-5") \ + .timeout(10).doc("5-2-6") \ + .success() + + # run the group + anet.run() + + def test_multiple_subscribers(self): + """This has more than one subscriber for the object.""" + if _debug: TestPulseConverter._debug("test_multiple_subscribers") + + # create a network + anet = ApplicationNetwork("test_multiple_subscribers") + + # add the ability to accept COV notifications to the TD + anet.td.add_capability(COVTestClientServices) + + # tell the TD how to respond to confirmed notifications + anet.td.test_ack = True + anet.td.test_reject = None + anet.td.test_abort = None + + # add the service capability to the IUT + anet.iut.add_capability(ChangeOfValueServices) + + # make a pulse converter object + test_pc = PulseConverterObject( + objectIdentifier=('pulseConverter', 1), + objectName='pc', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + updateTime=DateTime(date=Date().now().value, time=Time().now().value), + covIncrement=10.0, + covPeriod=10, + ) + + # an easy way to change both the present value and status flags + # which should trigger only one notification + def test_pc_fault(): + if _debug: TestPulseConverter._debug("test_pc_fault") + test_pc.presentValue = 100.0 + test_pc.statusFlags = [0, 0, 1, 0] + + # add it to the implementation + anet.iut.add_object(test_pc) + + # add another test device object + anet.td2_device_object = LocalDeviceObject( + objectName="td2", + objectIdentifier=('device', 30), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=999, + ) + + # another test device + anet.td2 = ApplicationStateMachine(anet.td2_device_object, anet.vlan) + anet.td2.add_capability(COVTestClientServices) + anet.append(anet.td2) + + # receive the subscription requests, wait until both clients have + # received the ack and the 'instant' notification. Then change the + # value, no ack coming back + anet.iut.start_state.doc("6-1-0") \ + .receive(SubscribeCOVRequest, pduSource=anet.td.address).doc("6-1-1") \ + .receive(SubscribeCOVRequest, pduSource=anet.td2.address).doc("6-1-2") \ + .wait_event("e2").doc("6-1-3") \ + .call(test_pc_fault).doc("6-1-4") \ + .timeout(10).doc("6-2-5") \ + .success() + + # first test device; send the subscription request, get an ack + # followed by the 'instant' notification + anet.td.start_state.doc("6-2-0") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-2-1") \ + .receive(SimpleAckPDU).doc("6-2-2") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-3") \ + .set_event("e1").doc("6-2-4") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-2-5") \ + .timeout(10).doc("6-2-6") \ + .success() + + # same pattern for the other test device + anet.td2.start_state.doc("6-3-0") \ + .wait_event("e1").doc("6-3-1") \ + .send(SubscribeCOVRequest( + destination=anet.iut.address, + subscriberProcessIdentifier=1, + monitoredObjectIdentifier=('pulseConverter', 1), + issueConfirmedNotifications=False, + lifetime=30, + )).doc("6-3-2") \ + .receive(SimpleAckPDU).doc("6-3-3") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-4") \ + .set_event("e2").doc("6-3-5") \ + .receive(UnconfirmedCOVNotificationRequest).doc("6-3-6") \ + .timeout(10).doc("6-3-7") \ + .success() + + # run the group + anet.run() +