diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 477e6836..70e98038 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.7' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py25/bacpypes/analysis.py b/py25/bacpypes/analysis.py index a285ae89..1a10b494 100755 --- a/py25/bacpypes/analysis.py +++ b/py25/bacpypes/analysis.py @@ -2,6 +2,14 @@ """ Analysis - Decoding pcap files + +Before analyzing files, install libpcap-dev: + + $ sudo apt install libpcap-dev + +then install pypcap: + + https://github.com/pynetwork/pypcap """ import sys @@ -15,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging +from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -33,13 +41,6 @@ socket.IPPROTO_UDP:'udp', socket.IPPROTO_ICMP:'icmp'} -# -# _hexify -# - -def _hexify(s, sep='.'): - return sep.join('%02X' % ord(c) for c in s) - # # strftimestamp # @@ -53,11 +54,11 @@ def strftimestamp(ts): # def decode_ethernet(s): - if _debug: decode_ethernet._debug("decode_ethernet %s...", _hexify(s[:14])) + if _debug: decode_ethernet._debug("decode_ethernet %s...", btox(s[:14])) d={} - d['destination_address'] = _hexify(s[0:6], ':') - d['source_address'] = _hexify(s[6:12], ':') + d['destination_address'] = btox(s[0:6], ':') + d['source_address'] = btox(s[6:12], ':') d['type'] = struct.unpack('!H',s[12:14])[0] d['data'] = s[14:] @@ -70,7 +71,7 @@ def decode_ethernet(s): # def decode_vlan(s): - if _debug: decode_vlan._debug("decode_vlan %s...", _hexify(s[:4])) + if _debug: decode_vlan._debug("decode_vlan %s...", btox(s[:4])) d = {} x = struct.unpack('!H',s[0:2])[0] @@ -89,7 +90,7 @@ def decode_vlan(s): # def decode_ip(s): - if _debug: decode_ip._debug("decode_ip %r", _hexify(s[:20])) + if _debug: decode_ip._debug("decode_ip %r", btox(s[:20])) d = {} d['version'] = (ord(s[0]) & 0xf0) >> 4 @@ -119,7 +120,7 @@ def decode_ip(s): # def decode_udp(s): - if _debug: decode_udp._debug("decode_udp %s...", _hexify(s[:8])) + if _debug: decode_udp._debug("decode_udp %s...", btox(s[:8])) d = {} d['source_port'] = struct.unpack('!H',s[0:2])[0] @@ -146,6 +147,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -176,10 +179,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -225,7 +226,7 @@ def decode_packet(data): # check for version number if (pdu.pduData[0] != '\x01'): - if _debug: decode_packet._debug(" - not a version 1 packet: %s...", _hexify(pdu.pduData[:30])) + if _debug: decode_packet._debug(" - not a version 1 packet: %s...", btox(pdu.pduData[:30])) return None # it's an NPDU @@ -355,33 +356,7 @@ def decode_file(fname): """Given the name of a pcap file, open it, decode the contents and yield each packet.""" if _debug: decode_file._debug("decode_file %r", fname) - if not pcap: - raise RuntimeError("failed to import pcap") - - # create a pcap object - p = pcap.pcapObject() - p.open_offline(fname) - - i = 0 - while 1: - # the object acts like an iterator - pkt = p.next() - if not pkt: - break - - # returns a tuple - pktlen, data, timestamp = pkt - pkt = decode_packet(data) - if not pkt: - continue - - # save the index and timestamp in the packet - pkt._index = i - pkt._timestamp = timestamp - - yield pkt - - i += 1 + raise NotImplementedError("not implemented") bacpypes_debugging(decode_file) @@ -389,7 +364,7 @@ def decode_file(fname): # Tracer # -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index e5dc0576..ae3659cf 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -357,12 +357,12 @@ def __init__(self, *args, **kwargs): super(APDU, self).__init__(*args, **kwargs) def encode(self, pdu): - if _debug: APCI._debug("encode %s", str(pdu)) + if _debug: APDU._debug("encode %s", str(pdu)) APCI.encode(self, pdu) pdu.put_data(self.pduData) def decode(self, pdu): - if _debug: APCI._debug("decode %s", str(pdu)) + if _debug: APDU._debug("decode %s", str(pdu)) APCI.decode(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) @@ -399,14 +399,20 @@ def dict_contents(self, use_dict=None, as_class=dict): class _APDU(APDU): def encode(self, pdu): + if _debug: _APDU._debug("encode %r", pdu) + APCI.update(pdu, self) pdu.put_data(self.pduData) def decode(self, pdu): + if _debug: _APDU._debug("decode %r", pdu) + APCI.update(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) def set_context(self, context): + if _debug: _APDU._debug("set_context %r", context) + self.pduUserData = context.pduUserData self.pduDestination = context.pduSource self.pduExpectingReply = 0 @@ -428,6 +434,8 @@ def __repr__(self): # put it together return "<%s(%s) instance at %s>" % (sname, stype, hex(id(self))) +bacpypes_debugging(_APDU) + # # ConfirmedRequestPDU # diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index 9d385249..d45c212b 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -107,8 +107,12 @@ class ObjectTypesSupported(BitString): , 'positiveIntegerValue':48 , 'timePatternValue':49 , 'timeValue':50 + , 'notificationForwarder':51 + , 'alertEnrollment':52 + , 'channel':53 + , 'lightingOutput':54 } - bitLen = 51 + bitLen = 55 class ResultFlags(BitString): bitNames = \ diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index f06d2eee..cdc096ec 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -77,7 +77,7 @@ def encode(self, taglist): """ """ if _debug: Sequence._debug("encode %r", taglist) - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -89,7 +89,7 @@ def encode(self, taglist): continue if not element.optional and value is None: raise MissingRequiredParameter("%s is a missing required element of %s" % (element.name, self.__class__.__name__)) - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # might need to encode an opening tag if element.context is not None: taglist.append(OpeningTag(element.context)) @@ -133,7 +133,10 @@ def encode(self, taglist): raise TypeError("%s must be of type %s" % (element.name, element.klass.__name__)) def decode(self, taglist): + """ + """ if _debug: Sequence._debug("decode %r", taglist) + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -141,13 +144,14 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: if element.optional: # omitted optional element setattr(self, element.name, None) - elif element.klass in _sequence_of_classes: + elif (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # empty list setattr(self, element.name, []) else: @@ -187,7 +191,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -284,7 +310,7 @@ def decode(self, taglist): raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) def debug_contents(self, indent=1, file=sys.stdout, _ids=None): - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes for element in self.sequenceElements: value = getattr(self, element.name, None) @@ -294,7 +320,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): file.write("%s%s is a missing required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): file.write("%s%s\n" % (" " * indent, element.name)) helper = element.klass(value) helper.debug_contents(indent+1, file, _ids) @@ -312,6 +338,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: Sequence._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + global _sequence_of_classes, _list_of_classes # make/extend the dictionary of content if use_dict is None: @@ -323,7 +350,7 @@ def dict_contents(self, use_dict=None, as_class=dict): if value is None: continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): helper = element.klass(value) mapped_value = helper.dict_contents(as_class=as_class) @@ -405,6 +432,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -495,6 +525,160 @@ def dict_contents(self, use_dict=None, as_class=dict): bacpypes_debugging(SequenceOf) +# +# List +# + +class List(object): + pass + +# +# ListOf +# + +_list_of_map = {} +_list_of_classes = {} + +def ListOf(klass): + """Function to return a class that can encode and decode a list of + some other type.""" + if _debug: ListOf._debug("ListOf %r", klass) + + global _list_of_map + global _list_of_classes, _array_of_classes + + # if this has already been built, return the cached one + if klass in _list_of_map: + if _debug: SequenceOf._debug(" - found in cache") + return _list_of_map[klass] + + # no ListOf(ListOf(...)) allowed + if klass in _list_of_classes: + raise TypeError("nested lists disallowed") + # no ListOf(ArrayOf(...)) allowed + if klass in _array_of_classes: + raise TypeError("lists of arrays disallowed") + + # define a generic class for lists + class _ListOf(List): + + subtype = None + + def __init__(self, value=None): + if _debug: _ListOf._debug("(%r)__init__ %r (subtype=%r)", self.__class__.__name__, value, self.subtype) + + if value is None: + self.value = [] + elif isinstance(value, list): + self.value = value + else: + raise TypeError("invalid constructor datatype") + + def append(self, value): + 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) + + def __len__(self): + return len(self.value) + + def __getitem__(self, item): + return self.value[item] + + def encode(self, taglist): + if _debug: _ListOf._debug("(%r)encode %r", self.__class__.__name__, taglist) + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + # a helper cooperates between the atomic value and the tag + helper = self.subtype(value) + + # build a tag and encode the data into it + tag = Tag() + helper.encode(tag) + + # now encode the tag + taglist.append(tag) + elif isinstance(value, self.subtype): + # it must have its own encoder + value.encode(taglist) + else: + raise TypeError("%s must be a %s" % (value, self.subtype.__name__)) + + def decode(self, taglist): + if _debug: _ListOf._debug("(%r)decode %r", self.__class__.__name__, taglist) + + while len(taglist) != 0: + tag = taglist.Peek() + if tag.tagClass == Tag.closingTagClass: + return + + if issubclass(self.subtype, (Atomic, AnyAtomic)): + if _debug: _ListOf._debug(" - building helper: %r %r", self.subtype, tag) + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = self.subtype(tag) + + # save the value + self.value.append(helper.value) + else: + if _debug: _ListOf._debug(" - building value: %r", self.subtype) + # build an element + value = self.subtype() + + # let it decode itself + value.decode(taglist) + + # save what was built + self.value.append(value) + + def debug_contents(self, indent=1, file=sys.stdout, _ids=None): + i = 0 + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + file.write("%s[%d] = %r\n" % (" " * indent, i, value)) + elif isinstance(value, self.subtype): + file.write("%s[%d]" % (" " * indent, i)) + value.debug_contents(indent+1, file, _ids) + else: + file.write("%s[%d] %s must be a %s" % (" " * indent, i, value, self.subtype.__name__)) + i += 1 + + def dict_contents(self, use_dict=None, as_class=dict): + # return sequences as arrays + mapped_value = [] + + for value in self.value: + if issubclass(self.subtype, Atomic): + mapped_value.append(value) ### ambiguous + elif issubclass(self.subtype, AnyAtomic): + mapped_value.append(value.value) ### ambiguous + elif isinstance(value, self.subtype): + mapped_value.append(value.dict_contents(as_class=as_class)) + + # return what we built + return mapped_value + + bacpypes_debugging(_ListOf) + + # constrain it to a list of a specific type of item + setattr(_ListOf, 'subtype', klass) + _ListOf.__name__ = 'ListOf' + klass.__name__ + if _debug: ListOf._debug(" - build this class: %r", _ListOf) + + # cache this type + _list_of_map[klass] = _ListOf + _list_of_classes[_ListOf] = 1 + + # return this new type + return _ListOf + +@bacpypes_debugging(ListOf) + # # Array # @@ -595,6 +779,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -847,6 +1034,7 @@ def encode(self, taglist): def decode(self, taglist): if _debug: Choice._debug("(%r)decode %r", self.__class__.__name__, taglist) + global _sequence_of_classes, _list_of_classes # peek at the element tag = taglist.Peek() @@ -863,7 +1051,7 @@ def decode(self, taglist): if _debug: Choice._debug(" - checking choice: %s", element.name) # check for a sequence element - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # check for context encoding if element.context is None: raise NotImplementedError("choice of a SequenceOf must be context encoded") @@ -1049,9 +1237,10 @@ def cast_in(self, element): def cast_out(self, klass): """Interpret the content as a particular class.""" if _debug: Any._debug("cast_out %r", klass) + global _sequence_of_classes, _list_of_classes # check for a sequence element - if klass in _sequence_of_classes: + if (klass in _sequence_of_classes) or (klass in _list_of_classes): # build a sequence helper helper = klass() @@ -1189,8 +1378,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py25/bacpypes/core.py b/py25/bacpypes/core.py index 1614a53d..30974ff3 100755 --- a/py25/bacpypes/core.py +++ b/py25/bacpypes/core.py @@ -52,10 +52,10 @@ def stop(*args): # dump_stack # -def dump_stack(): - if _debug: dump_stack._debug("dump_stack") +def dump_stack(debug_handler): + if _debug: dump_stack._debug("dump_stack %r", debug_handler) for filename, lineno, fn, _ in traceback.extract_stack()[:-1]: - sys.stderr.write(" %-20s %s:%s\n" % (fn, filename.split('/')[-1], lineno)) + debug_handler(" %-20s %s:%s", fn, filename.split('/')[-1], lineno) bacpypes_debugging(dump_stack) diff --git a/py25/bacpypes/local/__init__.py b/py25/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py25/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py new file mode 100644 index 00000000..7af52a7d --- /dev/null +++ b/py25/bacpypes/local/device.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentProtocolServicesSupported) + +# +# LocalDeviceObject +# + +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + +bacpypes_debugging(LocalDeviceObject) + diff --git a/py25/bacpypes/local/file.py b/py25/bacpypes/local/file.py new file mode 100644 index 00000000..3792526a --- /dev/null +++ b/py25/bacpypes/local/file.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +bacpypes_debugging(LocalRecordAccessFileObject) + +# +# Local Stream Access File Object Type +# + +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +bacpypes_debugging(LocalStreamAccessFileObject) + diff --git a/py25/bacpypes/local/object.py b/py25/bacpypes/local/object.py new file mode 100644 index 00000000..5b5be5d3 --- /dev/null +++ b/py25/bacpypes/local/object.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentPropertyList) + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py25/bacpypes/local/schedule.py b/py25/bacpypes/local/schedule.py new file mode 100644 index 00000000..a6f368be --- /dev/null +++ b/py25/bacpypes/local/schedule.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +bacpypes_debugging(date_in_calendar_entry) + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +bacpypes_debugging(LocalScheduleObject) + +# +# LocalScheduleInterpreter +# + +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + +bacpypes_debugging(LocalScheduleInterpreter) + diff --git a/py25/bacpypes/netservice.py b/py25/bacpypes/netservice.py index 4bdb81f5..76c5e021 100755 --- a/py25/bacpypes/netservice.py +++ b/py25/bacpypes/netservice.py @@ -4,7 +4,7 @@ Network Service """ -from copy import copy as _copy +from copy import deepcopy as _deepcopy from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError @@ -27,32 +27,135 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) + + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return - _debug_contents = ('adapter-', 'address', 'networks', 'status') + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) + + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] + +bacpypes_debugging(RouterInfoCache) # # NetworkAdapter @@ -63,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - 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) @@ -105,117 +205,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} + + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # 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: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - # make a key for the router reference - rkey = (adapter, address) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] - - ### check to see if it is OK to add the new entry + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref - - # reference the snet - self.networks[snet] = rref - - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +283,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +320,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +336,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return - ### make sure the direct connect is OK, may need to connect + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the peer router is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) + + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +390,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,14 +465,18 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: + if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") + # decode as a generic APDU apdu = _APDU(user_data=npdu.pduUserData) - apdu.decode(_copy(npdu)) + apdu.decode(_deepcopy(npdu)) if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -418,7 +485,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -444,34 +511,40 @@ def process_npdu(self, adapter, npdu): # pass upstream to the application layer self.response(apdu) - if not forwardMessage: - return else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) return + if _debug: NetworkServiceAccessPoint._debug(" - processing NPDU locally") + # do a deeper decode of the NPDU xpdu = npdu_types[npdu.npduNetMessage](user_data=npdu.pduUserData) - xpdu.decode(_copy(npdu)) + xpdu.decode(_deepcopy(npdu)) # pass to the service element self.sap_request(adapter, xpdu) - if not forwardMessage: - return + # might not need to forward this to other devices + if not forwardMessage: + if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") + return # make sure we're really a router if (len(self.adapters) == 1): + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters - newpdu = _copy(npdu) + newpdu = _deepcopy(npdu) # clear out the source and destination newpdu.pduSource = None @@ -488,48 +561,65 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): - xadapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(newpdu) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return - ### check to make sure the router is OK + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the network is OK, may need to connect + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - if _debug: NetworkServiceAccessPoint._debug(" - newpdu: %r", newpdu) + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -537,16 +627,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -619,17 +710,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue + + # add the direct network + netlist.append(xadapter.adapterNet) - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -644,42 +733,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") - - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -690,7 +783,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -698,8 +791,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) @@ -750,3 +881,4 @@ def DisconnectConnectionToNetwork(self, adapter, npdu): # sap = self.elementService bacpypes_debugging(NetworkServiceElement) + diff --git a/py25/bacpypes/npdu.py b/py25/bacpypes/npdu.py index 8e2ebaa8..69033ed8 100755 --- a/py25/bacpypes/npdu.py +++ b/py25/bacpypes/npdu.py @@ -479,7 +479,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -546,6 +546,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -738,6 +744,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -758,10 +769,17 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) npdu.put_short( self.nniNET ) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 3bbac83c..12b8d819 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -15,8 +15,8 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ Unsigned -from .constructeddata import AnyAtomic, Array, ArrayOf, Choice, Element, \ - Sequence, SequenceOf +from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ + Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ AccessEvent, AccessPassbackMode, AccessRule, AccessThreatLevel, \ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -154,7 +155,12 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr # keep the arguments self.identifier = identifier + + # check the datatype self.datatype = datatype + if not issubclass(datatype, (Atomic, Sequence, Choice, Array, List, AnyAtomic)): + raise TypeError("invalid datatype for property: %s" % (identifier,)) + self.optional = optional self.mutable = mutable self.default = default @@ -209,6 +215,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -255,6 +268,38 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # value is mutated into a new array value = self.datatype(value) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, List): + if _debug: Property._debug(" - property is list, checking subtype") + + # changing a single element + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # replacing the array + if not isinstance(value, list): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new list + value = self.datatype(value) + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") @@ -649,7 +694,7 @@ class AccessCredentialObject(Object): , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('credentialStatus', BinaryPV) - , ReadableProperty('reasonForDisable', SequenceOf(AccessCredentialDisableReason)) + , ReadableProperty('reasonForDisable', ListOf(AccessCredentialDisableReason)) , ReadableProperty('authenticationFactors', ArrayOf(CredentialAuthenticationFactor)) , ReadableProperty('activationTime', DateTime) , ReadableProperty('expiryTime', DateTime) @@ -665,7 +710,7 @@ class AccessCredentialObject(Object): , OptionalProperty('traceFlag', Boolean) , OptionalProperty('threatAuthority', AccessThreatLevel) , OptionalProperty('extendedTimeEnable', Boolean) - , OptionalProperty('authorizationExemptions', SequenceOf(AuthorizationException)) + , OptionalProperty('authorizationExemptions', ListOf(AuthorizationException)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) # , OptionalProperty('masterExemption', Boolean) # , OptionalProperty('passbackExemption', Boolean) @@ -693,12 +738,12 @@ class AccessDoorObject(Object): , OptionalProperty('doorUnlockDelayTime', Unsigned) , ReadableProperty('doorOpenTooLongTime', Unsigned) , OptionalProperty('doorAlarmState', DoorAlarmState) - , OptionalProperty('maskedAlarmValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('maskedAlarmValues', ListOf(DoorAlarmState)) , OptionalProperty('maintenanceRequired', Maintenance) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(DoorAlarmState)) - , OptionalProperty('faultValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('alarmValues', ListOf(DoorAlarmState)) + , OptionalProperty('faultValues', ListOf(DoorAlarmState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -729,7 +774,7 @@ class AccessPointObject(Object): , OptionalProperty('lockout', Boolean) , OptionalProperty('lockoutRelinquishTime', Unsigned) , OptionalProperty('failedAttempts', Unsigned) - , OptionalProperty('failedAttemptEvents', SequenceOf(AccessEvent)) + , OptionalProperty('failedAttemptEvents', ListOf(AccessEvent)) , OptionalProperty('maxFailedAttempts', Unsigned) , OptionalProperty('failedAttemptsTime', Unsigned) , OptionalProperty('threatLevel', AccessThreatLevel) @@ -749,8 +794,8 @@ class AccessPointObject(Object): , OptionalProperty('zoneFrom', DeviceObjectReference) , OptionalProperty('notificationClass', Unsigned) , OptionalProperty('transactionNotificationClass', Unsigned) - , OptionalProperty('accessAlarmEvents', SequenceOf(AccessEvent)) - , OptionalProperty('accessTransactionEvents', SequenceOf(AccessEvent)) + , OptionalProperty('accessAlarmEvents', ListOf(AccessEvent)) + , OptionalProperty('accessTransactionEvents', ListOf(AccessEvent)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -790,9 +835,9 @@ class AccessUserObject(Object): , OptionalProperty('userName', CharacterString) , OptionalProperty('userExternalIdentifier', CharacterString) , OptionalProperty('userInformationReference', CharacterString) - , OptionalProperty('members', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) - , ReadableProperty('credentials', SequenceOf(DeviceObjectReference)) + , OptionalProperty('members', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) + , ReadableProperty('credentials', ListOf(DeviceObjectReference)) ] register_object_type(AccessUserObject) @@ -811,18 +856,18 @@ class AccessZoneObject(Object): , OptionalProperty('adjustValue', Integer) , OptionalProperty('occupancyUpperLimit', Unsigned) , OptionalProperty('occupancyLowerLimit', Unsigned) - , OptionalProperty('credentialsInZone', SequenceOf(DeviceObjectReference) ) + , OptionalProperty('credentialsInZone', ListOf(DeviceObjectReference) ) , OptionalProperty('lastCredentialAdded', DeviceObjectReference) , OptionalProperty('lastCredentialAddedTime', DateTime) , OptionalProperty('lastCredentialRemoved', DeviceObjectReference) , OptionalProperty('lastCredentialRemovedTime', DateTime) , OptionalProperty('passbackMode', AccessPassbackMode) , OptionalProperty('passbackTimeout', Unsigned) - , ReadableProperty('entryPoints', SequenceOf(DeviceObjectReference)) - , ReadableProperty('exitPoints', SequenceOf(DeviceObjectReference)) + , ReadableProperty('entryPoints', ListOf(DeviceObjectReference)) + , ReadableProperty('exitPoints', ListOf(DeviceObjectReference)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(AccessZoneOccupancyState)) + , OptionalProperty('alarmValues', ListOf(AccessZoneOccupancyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1169,7 +1214,7 @@ class CalendarObject(Object): objectType = 'calendar' properties = \ [ ReadableProperty('presentValue', Boolean) - , ReadableProperty('dateList', SequenceOf(CalendarEntry)) + , ReadableProperty('dateList', ListOf(CalendarEntry)) ] register_object_type(CalendarObject) @@ -1342,8 +1387,8 @@ class DeviceObject(Object): , OptionalProperty('structuredObjectList', ArrayOf(ObjectIdentifier)) , ReadableProperty('maxApduLengthAccepted', Unsigned) , ReadableProperty('segmentationSupported', Segmentation) - , OptionalProperty('vtClassesSupported', SequenceOf(VTClass)) - , OptionalProperty('activeVtSessions', SequenceOf(VTSession)) + , OptionalProperty('vtClassesSupported', ListOf(VTClass)) + , OptionalProperty('activeVtSessions', ListOf(VTSession)) , OptionalProperty('localTime', Time) , OptionalProperty('localDate', Date) , OptionalProperty('utcOffset', Integer) @@ -1351,10 +1396,10 @@ class DeviceObject(Object): , OptionalProperty('apduSegmentTimeout', Unsigned) , ReadableProperty('apduTimeout', Unsigned) , ReadableProperty('numberOfApduRetries', Unsigned) - , OptionalProperty('timeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('timeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('maxMaster', Unsigned) , OptionalProperty('maxInfoFrames', Unsigned) - , ReadableProperty('deviceAddressBinding', SequenceOf(AddressBinding)) + , ReadableProperty('deviceAddressBinding', ListOf(AddressBinding)) , ReadableProperty('databaseRevision', Unsigned) , OptionalProperty('configurationFiles', ArrayOf(ObjectIdentifier)) , OptionalProperty('lastRestoreTime', TimeStamp) @@ -1363,16 +1408,16 @@ class DeviceObject(Object): , OptionalProperty('restorePreparationTime', Unsigned) , OptionalProperty('restoreCompletionTime', Unsigned) , OptionalProperty('backupAndRestoreState', BackupState) - , OptionalProperty('activeCovSubscriptions', SequenceOf(COVSubscription)) + , OptionalProperty('activeCovSubscriptions', ListOf(COVSubscription)) , OptionalProperty('maxSegmentsAccepted', Unsigned) , OptionalProperty('slaveProxyEnable', ArrayOf(Boolean)) , OptionalProperty('autoSlaveDiscovery', ArrayOf(Boolean)) - , OptionalProperty('slaveAddressBinding', SequenceOf(AddressBinding)) - , OptionalProperty('manualSlaveAddressBinding', SequenceOf(AddressBinding)) + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) , OptionalProperty('lastRestartReason', RestartReason) , OptionalProperty('timeOfDeviceRestart', TimeStamp) - , OptionalProperty('restartNotificationRecipients', SequenceOf(Recipient)) - , OptionalProperty('utcTimeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('restartNotificationRecipients', ListOf(Recipient)) + , OptionalProperty('utcTimeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) @@ -1434,7 +1479,7 @@ class EventLogObject(Object): , OptionalProperty('stopTime', DateTime) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(EventLogRecord)) + , ReadableProperty('logBuffer', ListOf(EventLogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) @@ -1500,7 +1545,7 @@ class GlobalGroupObject(Object): , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) , OptionalProperty('covuPeriod', Unsigned) - , OptionalProperty('covuRecipients', SequenceOf(Recipient)) + , OptionalProperty('covuRecipients', ListOf(Recipient)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) ] @@ -1509,8 +1554,8 @@ class GlobalGroupObject(Object): class GroupObject(Object): objectType = 'group' properties = \ - [ ReadableProperty('listOfGroupMembers', SequenceOf(ReadAccessSpecification)) - , ReadableProperty('presentValue', SequenceOf(ReadAccessResult)) + [ ReadableProperty('listOfGroupMembers', ListOf(ReadAccessSpecification)) + , ReadableProperty('presentValue', ListOf(ReadAccessResult)) ] register_object_type(GroupObject) @@ -1598,12 +1643,12 @@ class LifeSafetyPointObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1621,7 +1666,7 @@ class LifeSafetyPointObject(Object): , OptionalProperty('setting', Unsigned) , OptionalProperty('directReading', Real) , OptionalProperty('units', EngineeringUnits) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] register_object_type(LifeSafetyPointObject) @@ -1637,12 +1682,12 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1657,8 +1702,8 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('silenced', SilencedState) , ReadableProperty('operationExpected', LifeSafetyOperation) , OptionalProperty('maintenanceRequired', Boolean) - , ReadableProperty('zoneMembers', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , ReadableProperty('zoneMembers', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] register_object_type(LifeSafetyZoneObject) @@ -1789,8 +1834,8 @@ class MultiStateInputObject(Object): , OptionalProperty('stateText', ArrayOf(CharacterString)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1851,8 +1896,8 @@ class MultiStateValueObject(Object): , OptionalProperty('relinquishDefault', Unsigned) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1880,7 +1925,7 @@ class NetworkSecurityObject(Object): , WritableProperty('lastKeyServer', AddressBinding) , WritableProperty('securityPDUTimeout', Unsigned) , ReadableProperty('updateKeySetTimeout', Unsigned) - , ReadableProperty('supportedSecurityAlgorithms', SequenceOf(Unsigned)) + , ReadableProperty('supportedSecurityAlgorithms', ListOf(Unsigned)) , WritableProperty('doNotHide', Boolean) ] @@ -1892,7 +1937,7 @@ class NotificationClassObject(Object): [ ReadableProperty('notificationClass', Unsigned) , ReadableProperty('priority', ArrayOf(Unsigned)) , ReadableProperty('ackRequired', EventTransitionBits) - , ReadableProperty('recipientList', SequenceOf(Destination)) + , ReadableProperty('recipientList', ListOf(Destination)) ] register_object_type(NotificationClassObject) @@ -1903,8 +1948,8 @@ class NotificationForwarderObject(Object): [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) - , ReadableProperty('recipientList', SequenceOf(Destination)) - , WritableProperty('subscribedRecipients', SequenceOf(EventNotificationSubscription)) + , ReadableProperty('recipientList', ListOf(Destination)) + , WritableProperty('subscribedRecipients', ListOf(EventNotificationSubscription)) , ReadableProperty('processIdentifierFilter', ProcessIdSelection) , OptionalProperty('portFilter', ArrayOf(PortPermission)) , ReadableProperty('localForwardingOnly', Boolean) @@ -2035,7 +2080,7 @@ class ScheduleObject(Object): , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) - , ReadableProperty('listOfObjectPropertyReferences', SequenceOf(DeviceObjectPropertyReference)) + , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) , ReadableProperty('priorityForWriting', Unsigned) , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) @@ -2108,7 +2153,7 @@ class TrendLogObject(Object): , OptionalProperty('clientCovIncrement', ClientCOV) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogRecord)) + , ReadableProperty('logBuffer', ListOf(LogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , ReadableProperty('loggingType', LoggingType) @@ -2153,7 +2198,7 @@ class TrendLogMultipleObject(Object): , OptionalProperty('trigger', Boolean) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogMultipleRecord)) + , ReadableProperty('logBuffer', ListOf(LogMultipleRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 64070aa9..5367ed05 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -449,6 +452,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -461,6 +466,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1338,6 +1363,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1366,11 +1394,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, ''.join(chr(i) for i in self.value)) @@ -1450,19 +1498,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, ''.join(chr(c) for c in self.value)) @@ -1518,6 +1587,7 @@ class ObjectType(Enumerated): , 'accessUser':35 , 'accessZone':36 , 'accumulator':23 + , 'alertEnrollment':52 , 'analogInput':0 , 'analogOutput':1 , 'analogValue':2 @@ -1527,6 +1597,7 @@ class ObjectType(Enumerated): , 'binaryValue':5 , 'bitstringValue':39 , 'calendar':6 + , 'channel':53 , 'characterstringValue':40 , 'command':7 , 'credentialDataInput':37 @@ -1544,6 +1615,7 @@ class ObjectType(Enumerated): , 'largeAnalogValue':46 , 'lifeSafetyPoint':21 , 'lifeSafetyZone':22 + , 'lightingOutput':54 , 'loadControl':28 , 'loop':12 , 'multiStateInput':13 @@ -1551,6 +1623,7 @@ class ObjectType(Enumerated): , 'multiStateValue':19 , 'networkSecurity':38 , 'notificationClass':15 + , 'notificationForwarder':51 , 'octetstringValue':47 , 'positiveIntegerValue':48 , 'program':16 diff --git a/py25/bacpypes/service/cov.py b/py25/bacpypes/service/cov.py index c8792446..9e179ab7 100644 --- a/py25/bacpypes/service/cov.py +++ b/py25/bacpypes/service/cov.py @@ -12,7 +12,7 @@ from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ Recipient, RecipientProcess, ObjectPropertyReference -from ..constructeddata import SequenceOf, Any +from ..constructeddata import ListOf, Any from ..apdu import ConfirmedCOVNotificationRequest, \ UnconfirmedCOVNotificationRequest, \ SimpleAckPDU, Error, RejectPDU, AbortPDU @@ -422,7 +422,7 @@ class ActiveCOVSubscriptions(Property): def __init__(self): Property.__init__( - self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + self, 'activeCovSubscriptions', ListOf(COVSubscription), default=None, optional=True, mutable=False, ) @@ -434,7 +434,7 @@ def ReadProperty(self, obj, arrayIndex=None): if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) # start with an empty sequence - cov_subscriptions = SequenceOf(COVSubscription)() + cov_subscriptions = ListOf(COVSubscription)() # loop through the object and detection list for obj, cov_detection in obj._app.cov_detections.items(): diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py index 3c0e9bac..af3d29ae 100644 --- a/py25/bacpypes/service/device.py +++ b/py25/bacpypes/service/device.py @@ -4,137 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - -bacpypes_debugging(LocalDeviceObject) - # # Who-Is I-Am Services # diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 0ce857a7..a7fe647a 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -20,59 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -bacpypes_debugging(CurrentPropertyList) - -# -# CurrentPropertyListMixIn -# - -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - -bacpypes_debugging(CurrentPropertyListMixIn) - # # ReadProperty and WriteProperty Services # diff --git a/py25/bacpypes/vlan.py b/py25/bacpypes/vlan.py index e3d94069..4d716964 100755 --- a/py25/bacpypes/vlan.py +++ b/py25/bacpypes/vlan.py @@ -34,6 +34,9 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -58,6 +61,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 477e6836..70e98038 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.7' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py27/bacpypes/analysis.py b/py27/bacpypes/analysis.py index 711e7331..0840c6ac 100755 --- a/py27/bacpypes/analysis.py +++ b/py27/bacpypes/analysis.py @@ -2,6 +2,14 @@ """ Analysis - Decoding pcap files + +Before analyzing files, install libpcap-dev: + + $ sudo apt install libpcap-dev + +then install pypcap: + + https://github.com/pynetwork/pypcap """ import sys @@ -15,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging +from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -33,13 +41,6 @@ socket.IPPROTO_UDP:'udp', socket.IPPROTO_ICMP:'icmp'} -# -# _hexify -# - -def _hexify(s, sep='.'): - return sep.join('%02X' % ord(c) for c in s) - # # strftimestamp # @@ -54,11 +55,11 @@ def strftimestamp(ts): @bacpypes_debugging def decode_ethernet(s): - if _debug: decode_ethernet._debug("decode_ethernet %s...", _hexify(s[:14])) + if _debug: decode_ethernet._debug("decode_ethernet %s...", btox(s[:14])) d={} - d['destination_address'] = _hexify(s[0:6], ':') - d['source_address'] = _hexify(s[6:12], ':') + d['destination_address'] = btox(s[0:6], ':') + d['source_address'] = btox(s[6:12], ':') d['type'] = struct.unpack('!H',s[12:14])[0] d['data'] = s[14:] @@ -70,7 +71,7 @@ def decode_ethernet(s): @bacpypes_debugging def decode_vlan(s): - if _debug: decode_vlan._debug("decode_vlan %s...", _hexify(s[:4])) + if _debug: decode_vlan._debug("decode_vlan %s...", btox(s[:4])) d = {} x = struct.unpack('!H',s[0:2])[0] @@ -88,7 +89,7 @@ def decode_vlan(s): @bacpypes_debugging def decode_ip(s): - if _debug: decode_ip._debug("decode_ip %r", _hexify(s[:20])) + if _debug: decode_ip._debug("decode_ip %r", btox(s[:20])) d = {} d['version'] = (ord(s[0]) & 0xf0) >> 4 @@ -117,7 +118,7 @@ def decode_ip(s): @bacpypes_debugging def decode_udp(s): - if _debug: decode_udp._debug("decode_udp %s...", _hexify(s[:8])) + if _debug: decode_udp._debug("decode_udp %s...", btox(s[:8])) d = {} d['source_port'] = struct.unpack('!H',s[0:2])[0] @@ -143,6 +144,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -173,10 +176,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -222,7 +223,7 @@ def decode_packet(data): # check for version number if (pdu.pduData[0] != '\x01'): - if _debug: decode_packet._debug(" - not a version 1 packet: %s...", _hexify(pdu.pduData[:30])) + if _debug: decode_packet._debug(" - not a version 1 packet: %s...", btox(pdu.pduData[:30])) return None # it's an NPDU @@ -355,36 +356,25 @@ def decode_file(fname): raise RuntimeError("failed to import pcap") # create a pcap object - p = pcap.pcapObject() - p.open_offline(fname) + p = pcap.pcap(fname) - i = 0 - while 1: - # the object acts like an iterator - pkt = p.next() - if not pkt: - break - - # returns a tuple - pktlen, data, timestamp = pkt + for timestamp, data in p: pkt = decode_packet(data) if not pkt: continue # save the index and timestamp in the packet - pkt._index = i + # pkt._index = i pkt._timestamp = timestamp yield pkt - i += 1 - # # Tracer # @bacpypes_debugging -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index c01e7b97..10706aa2 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -349,6 +349,7 @@ def apci_contents(self, use_dict=None, as_class=dict): # APDU # +@bacpypes_debugging class APDU(APCI, PDUData): def __init__(self, *args, **kwargs): @@ -356,12 +357,13 @@ def __init__(self, *args, **kwargs): super(APDU, self).__init__(*args, **kwargs) def encode(self, pdu): - if _debug: APCI._debug("encode %s", str(pdu)) + if _debug: APDU._debug("encode %s", str(pdu)) APCI.encode(self, pdu) pdu.put_data(self.pduData) def decode(self, pdu): - if _debug: APCI._debug("decode %s", str(pdu)) + if _debug: APDU._debug("decode %s", str(pdu)) + APCI.decode(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) @@ -393,17 +395,24 @@ def dict_contents(self, use_dict=None, as_class=dict): # between PDU's. Otherwise the APCI content would be decoded twice. # +@bacpypes_debugging class _APDU(APDU): def encode(self, pdu): + if _debug: _APDU._debug("encode %r", pdu) + APCI.update(pdu, self) pdu.put_data(self.pduData) def decode(self, pdu): + if _debug: _APDU._debug("decode %r", pdu) + APCI.update(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) def set_context(self, context): + if _debug: _APDU._debug("set_context %r", context) + self.pduUserData = context.pduUserData self.pduDestination = context.pduSource self.pduExpectingReply = 0 diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index 9d385249..d45c212b 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -107,8 +107,12 @@ class ObjectTypesSupported(BitString): , 'positiveIntegerValue':48 , 'timePatternValue':49 , 'timeValue':50 + , 'notificationForwarder':51 + , 'alertEnrollment':52 + , 'channel':53 + , 'lightingOutput':54 } - bitLen = 51 + bitLen = 55 class ResultFlags(BitString): bitNames = \ diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 88fe28de..bad87d0d 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -78,7 +78,7 @@ def encode(self, taglist): """ """ if _debug: Sequence._debug("encode %r", taglist) - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -90,7 +90,7 @@ def encode(self, taglist): continue if not element.optional and value is None: raise MissingRequiredParameter("%s is a missing required element of %s" % (element.name, self.__class__.__name__)) - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # might need to encode an opening tag if element.context is not None: taglist.append(OpeningTag(element.context)) @@ -134,7 +134,10 @@ def encode(self, taglist): raise TypeError("%s must be of type %s" % (element.name, element.klass.__name__)) def decode(self, taglist): + """ + """ if _debug: Sequence._debug("decode %r", taglist) + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -142,13 +145,14 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: if element.optional: # omitted optional element setattr(self, element.name, None) - elif element.klass in _sequence_of_classes: + elif (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # empty list setattr(self, element.name, []) else: @@ -188,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -285,7 +311,7 @@ def decode(self, taglist): raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) def debug_contents(self, indent=1, file=sys.stdout, _ids=None): - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes for element in self.sequenceElements: value = getattr(self, element.name, None) @@ -295,7 +321,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): file.write("%s%s is a missing required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): file.write("%s%s\n" % (" " * indent, element.name)) helper = element.klass(value) helper.debug_contents(indent+1, file, _ids) @@ -313,6 +339,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: Sequence._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + global _sequence_of_classes, _list_of_classes # make/extend the dictionary of content if use_dict is None: @@ -324,7 +351,7 @@ def dict_contents(self, use_dict=None, as_class=dict): if value is None: continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): helper = element.klass(value) mapped_value = helper.dict_contents(as_class=as_class) @@ -406,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -492,6 +522,158 @@ def dict_contents(self, use_dict=None, as_class=dict): # return this new type return _SequenceOf +# +# List +# + +class List(object): + pass + +# +# ListOf +# + +_list_of_map = {} +_list_of_classes = {} + +@bacpypes_debugging +def ListOf(klass): + """Function to return a class that can encode and decode a list of + some other type.""" + if _debug: ListOf._debug("ListOf %r", klass) + + global _list_of_map + global _list_of_classes, _array_of_classes + + # if this has already been built, return the cached one + if klass in _list_of_map: + if _debug: SequenceOf._debug(" - found in cache") + return _list_of_map[klass] + + # no ListOf(ListOf(...)) allowed + if klass in _list_of_classes: + raise TypeError("nested lists disallowed") + # no ListOf(ArrayOf(...)) allowed + if klass in _array_of_classes: + raise TypeError("lists of arrays disallowed") + + # define a generic class for lists + @bacpypes_debugging + class _ListOf(List): + + subtype = None + + def __init__(self, value=None): + if _debug: _ListOf._debug("(%r)__init__ %r (subtype=%r)", self.__class__.__name__, value, self.subtype) + + if value is None: + self.value = [] + elif isinstance(value, list): + self.value = value + else: + raise TypeError("invalid constructor datatype") + + def append(self, value): + 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) + + def __len__(self): + return len(self.value) + + def __getitem__(self, item): + return self.value[item] + + def encode(self, taglist): + if _debug: _ListOf._debug("(%r)encode %r", self.__class__.__name__, taglist) + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + # a helper cooperates between the atomic value and the tag + helper = self.subtype(value) + + # build a tag and encode the data into it + tag = Tag() + helper.encode(tag) + + # now encode the tag + taglist.append(tag) + elif isinstance(value, self.subtype): + # it must have its own encoder + value.encode(taglist) + else: + raise TypeError("%s must be a %s" % (value, self.subtype.__name__)) + + def decode(self, taglist): + if _debug: _ListOf._debug("(%r)decode %r", self.__class__.__name__, taglist) + + while len(taglist) != 0: + tag = taglist.Peek() + if tag.tagClass == Tag.closingTagClass: + return + + if issubclass(self.subtype, (Atomic, AnyAtomic)): + if _debug: _ListOf._debug(" - building helper: %r %r", self.subtype, tag) + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = self.subtype(tag) + + # save the value + self.value.append(helper.value) + else: + if _debug: _ListOf._debug(" - building value: %r", self.subtype) + # build an element + value = self.subtype() + + # let it decode itself + value.decode(taglist) + + # save what was built + self.value.append(value) + + def debug_contents(self, indent=1, file=sys.stdout, _ids=None): + i = 0 + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + file.write("%s[%d] = %r\n" % (" " * indent, i, value)) + elif isinstance(value, self.subtype): + file.write("%s[%d]" % (" " * indent, i)) + value.debug_contents(indent+1, file, _ids) + else: + file.write("%s[%d] %s must be a %s" % (" " * indent, i, value, self.subtype.__name__)) + i += 1 + + def dict_contents(self, use_dict=None, as_class=dict): + # return sequences as arrays + mapped_value = [] + + for value in self.value: + if issubclass(self.subtype, Atomic): + mapped_value.append(value) ### ambiguous + elif issubclass(self.subtype, AnyAtomic): + mapped_value.append(value.value) ### ambiguous + elif isinstance(value, self.subtype): + mapped_value.append(value.dict_contents(as_class=as_class)) + + # return what we built + return mapped_value + + # constrain it to a list of a specific type of item + setattr(_ListOf, 'subtype', klass) + _ListOf.__name__ = 'ListOf' + klass.__name__ + if _debug: ListOf._debug(" - build this class: %r", _ListOf) + + # cache this type + _list_of_map[klass] = _ListOf + _list_of_classes[_ListOf] = 1 + + # return this new type + return _ListOf + # # Array # @@ -593,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -844,6 +1029,7 @@ def encode(self, taglist): def decode(self, taglist): if _debug: Choice._debug("(%r)decode %r", self.__class__.__name__, taglist) + global _sequence_of_classes, _list_of_classes # peek at the element tag = taglist.Peek() @@ -860,7 +1046,7 @@ def decode(self, taglist): if _debug: Choice._debug(" - checking choice: %s", element.name) # check for a sequence element - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # check for context encoding if element.context is None: raise NotImplementedError("choice of a SequenceOf must be context encoded") @@ -1045,9 +1231,10 @@ def cast_in(self, element): def cast_out(self, klass): """Interpret the content as a particular class.""" if _debug: Any._debug("cast_out %r", klass) + global _sequence_of_classes, _list_of_classes # check for a sequence element - if klass in _sequence_of_classes: + if (klass in _sequence_of_classes) or (klass in _list_of_classes): # build a sequence helper helper = klass() @@ -1153,7 +1340,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) @@ -1184,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py27/bacpypes/core.py b/py27/bacpypes/core.py index b2c46e34..6430b864 100755 --- a/py27/bacpypes/core.py +++ b/py27/bacpypes/core.py @@ -52,10 +52,10 @@ def stop(*args): # @bacpypes_debugging -def dump_stack(): - if _debug: dump_stack._debug("dump_stack") +def dump_stack(debug_handler): + if _debug: dump_stack._debug("dump_stack %r", debug_handler) for filename, lineno, fn, _ in traceback.extract_stack()[:-1]: - sys.stderr.write(" %-20s %s:%s\n" % (fn, filename.split('/')[-1], lineno)) + debug_handler(" %-20s %s:%s", fn, filename.split('/')[-1], lineno) # # print_stack diff --git a/py27/bacpypes/local/__init__.py b/py27/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py27/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py27/bacpypes/local/device.py b/py27/bacpypes/local/device.py new file mode 100644 index 00000000..f1aef729 --- /dev/null +++ b/py27/bacpypes/local/device.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + diff --git a/py27/bacpypes/local/file.py b/py27/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py27/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py27/bacpypes/local/object.py b/py27/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py27/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py27/bacpypes/local/schedule.py b/py27/bacpypes/local/schedule.py new file mode 100644 index 00000000..91cbee62 --- /dev/null +++ b/py27/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py27/bacpypes/netservice.py b/py27/bacpypes/netservice.py index 6d02f687..391abea7 100755 --- a/py27/bacpypes/netservice.py +++ b/py27/bacpypes/netservice.py @@ -4,7 +4,7 @@ Network Service """ -from copy import copy as _copy +from copy import deepcopy as _deepcopy from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError @@ -27,32 +27,134 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +@bacpypes_debugging +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) - _debug_contents = ('adapter-', 'address', 'networks', 'status') + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") + + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) + + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] # # NetworkAdapter @@ -64,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - 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) @@ -105,117 +204,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} + + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # 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: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - # make a key for the router reference - rkey = (adapter, address) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] - - ### check to see if it is OK to add the new entry + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref - - # reference the snet - self.networks[snet] = rref - - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +282,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +319,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +335,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return - ### make sure the direct connect is OK, may need to connect + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the peer router is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) + + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +389,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,14 +464,18 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: + if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") + # decode as a generic APDU apdu = _APDU(user_data=npdu.pduUserData) - apdu.decode(_copy(npdu)) + apdu.decode(_deepcopy(npdu)) if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -418,7 +484,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -444,34 +510,40 @@ def process_npdu(self, adapter, npdu): # pass upstream to the application layer self.response(apdu) - if not forwardMessage: - return else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) return + if _debug: NetworkServiceAccessPoint._debug(" - processing NPDU locally") + # do a deeper decode of the NPDU xpdu = npdu_types[npdu.npduNetMessage](user_data=npdu.pduUserData) - xpdu.decode(_copy(npdu)) + xpdu.decode(_deepcopy(npdu)) # pass to the service element self.sap_request(adapter, xpdu) - if not forwardMessage: - return + # might not need to forward this to other devices + if not forwardMessage: + if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") + return # make sure we're really a router if (len(self.adapters) == 1): + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters - newpdu = _copy(npdu) + newpdu = _deepcopy(npdu) # clear out the source and destination newpdu.pduSource = None @@ -488,48 +560,65 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): - xadapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(newpdu) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return - ### check to make sure the router is OK + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the network is OK, may need to connect + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - if _debug: NetworkServiceAccessPoint._debug(" - newpdu: %r", newpdu) + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -537,16 +626,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -618,17 +708,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue + + # add the direct network + netlist.append(xadapter.adapterNet) - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -643,42 +731,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") - - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -689,7 +781,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -697,8 +789,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) diff --git a/py27/bacpypes/npdu.py b/py27/bacpypes/npdu.py index c6ad9d97..436f6bb8 100755 --- a/py27/bacpypes/npdu.py +++ b/py27/bacpypes/npdu.py @@ -476,7 +476,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -543,6 +543,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -735,6 +741,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -755,25 +766,32 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) - npdu.put_short( self.nniNET ) + npdu.put_short( self.nniNet ) npdu.put( self.nniFlag ) def decode(self, npdu): NPCI.update(self, npdu) - self.nniNET = npdu.get_short() + self.nniNet = npdu.get_short() self.nniFlag = npdu.get() def npdu_contents(self, use_dict=None, as_class=dict): return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( ('function', 'NetorkNumberIs'), - ('net', self.nniNET), + ('net', self.nniNet), ('flag', self.nniFlag), )) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 2cb56485..dc09e736 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -15,8 +15,8 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ Unsigned -from .constructeddata import AnyAtomic, Array, ArrayOf, Choice, Element, \ - Sequence, SequenceOf +from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ + Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ AccessEvent, AccessPassbackMode, AccessRule, AccessThreatLevel, \ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ @@ -156,7 +156,12 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr # keep the arguments self.identifier = identifier + + # check the datatype self.datatype = datatype + if not issubclass(datatype, (Atomic, Sequence, Choice, Array, List, AnyAtomic)): + raise TypeError("invalid datatype for property: %s" % (identifier,)) + self.optional = optional self.mutable = mutable self.default = default @@ -211,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -257,6 +269,38 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # value is mutated into a new array value = self.datatype(value) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, List): + if _debug: Property._debug(" - property is list, checking subtype") + + # changing a single element + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # replacing the array + if not isinstance(value, list): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new list + value = self.datatype(value) + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") @@ -658,7 +702,7 @@ class AccessCredentialObject(Object): , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('credentialStatus', BinaryPV) - , ReadableProperty('reasonForDisable', SequenceOf(AccessCredentialDisableReason)) + , ReadableProperty('reasonForDisable', ListOf(AccessCredentialDisableReason)) , ReadableProperty('authenticationFactors', ArrayOf(CredentialAuthenticationFactor)) , ReadableProperty('activationTime', DateTime) , ReadableProperty('expiryTime', DateTime) @@ -674,7 +718,7 @@ class AccessCredentialObject(Object): , OptionalProperty('traceFlag', Boolean) , OptionalProperty('threatAuthority', AccessThreatLevel) , OptionalProperty('extendedTimeEnable', Boolean) - , OptionalProperty('authorizationExemptions', SequenceOf(AuthorizationException)) + , OptionalProperty('authorizationExemptions', ListOf(AuthorizationException)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) # , OptionalProperty('masterExemption', Boolean) # , OptionalProperty('passbackExemption', Boolean) @@ -701,12 +745,12 @@ class AccessDoorObject(Object): , OptionalProperty('doorUnlockDelayTime', Unsigned) , ReadableProperty('doorOpenTooLongTime', Unsigned) , OptionalProperty('doorAlarmState', DoorAlarmState) - , OptionalProperty('maskedAlarmValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('maskedAlarmValues', ListOf(DoorAlarmState)) , OptionalProperty('maintenanceRequired', Maintenance) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(DoorAlarmState)) - , OptionalProperty('faultValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('alarmValues', ListOf(DoorAlarmState)) + , OptionalProperty('faultValues', ListOf(DoorAlarmState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -736,7 +780,7 @@ class AccessPointObject(Object): , OptionalProperty('lockout', Boolean) , OptionalProperty('lockoutRelinquishTime', Unsigned) , OptionalProperty('failedAttempts', Unsigned) - , OptionalProperty('failedAttemptEvents', SequenceOf(AccessEvent)) + , OptionalProperty('failedAttemptEvents', ListOf(AccessEvent)) , OptionalProperty('maxFailedAttempts', Unsigned) , OptionalProperty('failedAttemptsTime', Unsigned) , OptionalProperty('threatLevel', AccessThreatLevel) @@ -756,8 +800,8 @@ class AccessPointObject(Object): , OptionalProperty('zoneFrom', DeviceObjectReference) , OptionalProperty('notificationClass', Unsigned) , OptionalProperty('transactionNotificationClass', Unsigned) - , OptionalProperty('accessAlarmEvents', SequenceOf(AccessEvent)) - , OptionalProperty('accessTransactionEvents', SequenceOf(AccessEvent)) + , OptionalProperty('accessAlarmEvents', ListOf(AccessEvent)) + , OptionalProperty('accessTransactionEvents', ListOf(AccessEvent)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -795,9 +839,9 @@ class AccessUserObject(Object): , OptionalProperty('userName', CharacterString) , OptionalProperty('userExternalIdentifier', CharacterString) , OptionalProperty('userInformationReference', CharacterString) - , OptionalProperty('members', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) - , ReadableProperty('credentials', SequenceOf(DeviceObjectReference)) + , OptionalProperty('members', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) + , ReadableProperty('credentials', ListOf(DeviceObjectReference)) ] @register_object_type @@ -815,18 +859,18 @@ class AccessZoneObject(Object): , OptionalProperty('adjustValue', Integer) , OptionalProperty('occupancyUpperLimit', Unsigned) , OptionalProperty('occupancyLowerLimit', Unsigned) - , OptionalProperty('credentialsInZone', SequenceOf(DeviceObjectReference) ) + , OptionalProperty('credentialsInZone', ListOf(DeviceObjectReference) ) , OptionalProperty('lastCredentialAdded', DeviceObjectReference) , OptionalProperty('lastCredentialAddedTime', DateTime) , OptionalProperty('lastCredentialRemoved', DeviceObjectReference) , OptionalProperty('lastCredentialRemovedTime', DateTime) , OptionalProperty('passbackMode', AccessPassbackMode) , OptionalProperty('passbackTimeout', Unsigned) - , ReadableProperty('entryPoints', SequenceOf(DeviceObjectReference)) - , ReadableProperty('exitPoints', SequenceOf(DeviceObjectReference)) + , ReadableProperty('entryPoints', ListOf(DeviceObjectReference)) + , ReadableProperty('exitPoints', ListOf(DeviceObjectReference)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(AccessZoneOccupancyState)) + , OptionalProperty('alarmValues', ListOf(AccessZoneOccupancyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1162,7 +1206,7 @@ class CalendarObject(Object): objectType = 'calendar' properties = \ [ ReadableProperty('presentValue', Boolean) - , ReadableProperty('dateList', SequenceOf(CalendarEntry)) + , ReadableProperty('dateList', ListOf(CalendarEntry)) ] @register_object_type @@ -1326,8 +1370,8 @@ class DeviceObject(Object): , OptionalProperty('structuredObjectList', ArrayOf(ObjectIdentifier)) , ReadableProperty('maxApduLengthAccepted', Unsigned) , ReadableProperty('segmentationSupported', Segmentation) - , OptionalProperty('vtClassesSupported', SequenceOf(VTClass)) - , OptionalProperty('activeVtSessions', SequenceOf(VTSession)) + , OptionalProperty('vtClassesSupported', ListOf(VTClass)) + , OptionalProperty('activeVtSessions', ListOf(VTSession)) , OptionalProperty('localTime', Time) , OptionalProperty('localDate', Date) , OptionalProperty('utcOffset', Integer) @@ -1335,10 +1379,10 @@ class DeviceObject(Object): , OptionalProperty('apduSegmentTimeout', Unsigned) , ReadableProperty('apduTimeout', Unsigned) , ReadableProperty('numberOfApduRetries', Unsigned) - , OptionalProperty('timeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('timeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('maxMaster', Unsigned) , OptionalProperty('maxInfoFrames', Unsigned) - , ReadableProperty('deviceAddressBinding', SequenceOf(AddressBinding)) + , ReadableProperty('deviceAddressBinding', ListOf(AddressBinding)) , ReadableProperty('databaseRevision', Unsigned) , OptionalProperty('configurationFiles', ArrayOf(ObjectIdentifier)) , OptionalProperty('lastRestoreTime', TimeStamp) @@ -1347,16 +1391,16 @@ class DeviceObject(Object): , OptionalProperty('restorePreparationTime', Unsigned) , OptionalProperty('restoreCompletionTime', Unsigned) , OptionalProperty('backupAndRestoreState', BackupState) - , OptionalProperty('activeCovSubscriptions', SequenceOf(COVSubscription)) + , OptionalProperty('activeCovSubscriptions', ListOf(COVSubscription)) , OptionalProperty('maxSegmentsAccepted', Unsigned) , OptionalProperty('slaveProxyEnable', ArrayOf(Boolean)) , OptionalProperty('autoSlaveDiscovery', ArrayOf(Boolean)) - , OptionalProperty('slaveAddressBinding', SequenceOf(AddressBinding)) - , OptionalProperty('manualSlaveAddressBinding', SequenceOf(AddressBinding)) + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) , OptionalProperty('lastRestartReason', RestartReason) , OptionalProperty('timeOfDeviceRestart', TimeStamp) - , OptionalProperty('restartNotificationRecipients', SequenceOf(Recipient)) - , OptionalProperty('utcTimeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('restartNotificationRecipients', ListOf(Recipient)) + , OptionalProperty('utcTimeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) @@ -1416,7 +1460,7 @@ class EventLogObject(Object): , OptionalProperty('stopTime', DateTime) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(EventLogRecord)) + , ReadableProperty('logBuffer', ListOf(EventLogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) @@ -1480,7 +1524,7 @@ class GlobalGroupObject(Object): , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) , OptionalProperty('covuPeriod', Unsigned) - , OptionalProperty('covuRecipients', SequenceOf(Recipient)) + , OptionalProperty('covuRecipients', ListOf(Recipient)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) ] @@ -1488,8 +1532,8 @@ class GlobalGroupObject(Object): class GroupObject(Object): objectType = 'group' properties = \ - [ ReadableProperty('listOfGroupMembers', SequenceOf(ReadAccessSpecification)) - , ReadableProperty('presentValue', SequenceOf(ReadAccessResult)) + [ ReadableProperty('listOfGroupMembers', ListOf(ReadAccessSpecification)) + , ReadableProperty('presentValue', ListOf(ReadAccessResult)) ] @register_object_type @@ -1574,12 +1618,12 @@ class LifeSafetyPointObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1597,7 +1641,7 @@ class LifeSafetyPointObject(Object): , OptionalProperty('setting', Unsigned) , OptionalProperty('directReading', Real) , OptionalProperty('units', EngineeringUnits) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] @register_object_type @@ -1612,12 +1656,12 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1632,8 +1676,8 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('silenced', SilencedState) , ReadableProperty('operationExpected', LifeSafetyOperation) , OptionalProperty('maintenanceRequired', Boolean) - , ReadableProperty('zoneMembers', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , ReadableProperty('zoneMembers', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] @register_object_type @@ -1760,8 +1804,8 @@ class MultiStateInputObject(Object): , OptionalProperty('stateText', ArrayOf(CharacterString)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1820,8 +1864,8 @@ class MultiStateValueObject(Object): , OptionalProperty('relinquishDefault', Unsigned) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1848,7 +1892,7 @@ class NetworkSecurityObject(Object): , WritableProperty('lastKeyServer', AddressBinding) , WritableProperty('securityPDUTimeout', Unsigned) , ReadableProperty('updateKeySetTimeout', Unsigned) - , ReadableProperty('supportedSecurityAlgorithms', SequenceOf(Unsigned)) + , ReadableProperty('supportedSecurityAlgorithms', ListOf(Unsigned)) , WritableProperty('doNotHide', Boolean) ] @@ -1859,7 +1903,7 @@ class NotificationClassObject(Object): [ ReadableProperty('notificationClass', Unsigned) , ReadableProperty('priority', ArrayOf(Unsigned)) , ReadableProperty('ackRequired', EventTransitionBits) - , ReadableProperty('recipientList', SequenceOf(Destination)) + , ReadableProperty('recipientList', ListOf(Destination)) ] @register_object_type @@ -1869,8 +1913,8 @@ class NotificationForwarderObject(Object): [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) - , ReadableProperty('recipientList', SequenceOf(Destination)) - , WritableProperty('subscribedRecipients', SequenceOf(EventNotificationSubscription)) + , ReadableProperty('recipientList', ListOf(Destination)) + , WritableProperty('subscribedRecipients', ListOf(EventNotificationSubscription)) , ReadableProperty('processIdentifierFilter', ProcessIdSelection) , OptionalProperty('portFilter', ArrayOf(PortPermission)) , ReadableProperty('localForwardingOnly', Boolean) @@ -1996,7 +2040,7 @@ class ScheduleObject(Object): , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) - , ReadableProperty('listOfObjectPropertyReferences', SequenceOf(DeviceObjectPropertyReference)) + , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) , ReadableProperty('priorityForWriting', Unsigned) , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) @@ -2065,7 +2109,7 @@ class TrendLogObject(Object): , OptionalProperty('clientCovIncrement', ClientCOV) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogRecord)) + , ReadableProperty('logBuffer', ListOf(LogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , ReadableProperty('loggingType', LoggingType) @@ -2109,7 +2153,7 @@ class TrendLogMultipleObject(Object): , OptionalProperty('trigger', Boolean) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogMultipleRecord)) + , ReadableProperty('logBuffer', ListOf(LogMultipleRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 91c10d7d..ccc0d688 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -453,6 +456,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -465,6 +470,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1344,6 +1369,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1372,11 +1400,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1456,19 +1504,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) @@ -1524,6 +1593,7 @@ class ObjectType(Enumerated): , 'accessUser':35 , 'accessZone':36 , 'accumulator':23 + , 'alertEnrollment':52 , 'analogInput':0 , 'analogOutput':1 , 'analogValue':2 @@ -1533,6 +1603,7 @@ class ObjectType(Enumerated): , 'binaryValue':5 , 'bitstringValue':39 , 'calendar':6 + , 'channel':53 , 'characterstringValue':40 , 'command':7 , 'credentialDataInput':37 @@ -1550,6 +1621,7 @@ class ObjectType(Enumerated): , 'largeAnalogValue':46 , 'lifeSafetyPoint':21 , 'lifeSafetyZone':22 + , 'lightingOutput':54 , 'loadControl':28 , 'loop':12 , 'multiStateInput':13 @@ -1557,6 +1629,7 @@ class ObjectType(Enumerated): , 'multiStateValue':19 , 'networkSecurity':38 , 'notificationClass':15 + , 'notificationForwarder':51 , 'octetstringValue':47 , 'positiveIntegerValue':48 , 'program':16 diff --git a/py27/bacpypes/service/cov.py b/py27/bacpypes/service/cov.py index 438e0b22..7e14cb70 100644 --- a/py27/bacpypes/service/cov.py +++ b/py27/bacpypes/service/cov.py @@ -12,7 +12,7 @@ from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ Recipient, RecipientProcess, ObjectPropertyReference -from ..constructeddata import SequenceOf, Any +from ..constructeddata import ListOf, Any from ..apdu import ConfirmedCOVNotificationRequest, \ UnconfirmedCOVNotificationRequest, \ SimpleAckPDU, Error, RejectPDU, AbortPDU @@ -420,7 +420,7 @@ class ActiveCOVSubscriptions(Property): def __init__(self): Property.__init__( - self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + self, 'activeCovSubscriptions', ListOf(COVSubscription), default=None, optional=True, mutable=False, ) @@ -432,7 +432,7 @@ def ReadProperty(self, obj, arrayIndex=None): if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) # start with an empty sequence - cov_subscriptions = SequenceOf(COVSubscription)() + cov_subscriptions = ListOf(COVSubscription)() # loop through the object and detection list for obj, cov_detection in obj._app.cov_detections.items(): diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 25283a4d..11be52b0 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for properties this class implements - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index ca8d3fe9..5c4c993f 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -7,11 +7,11 @@ from ..primitivedata import Atomic, Null, Unsigned from ..constructeddata import Any, Array, ArrayOf -from ..apdu import Error, \ +from ..apdu import \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import Property, Object, PropertyError +from ..object import PropertyError # some debugging _debug = 0 @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index 953e266e..ead2d03a 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -35,6 +35,9 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -59,6 +62,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index bd516040..a77e8be9 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.16.7' +__version__ = '0.17.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index 711e7331..7b64a3f0 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -2,6 +2,14 @@ """ Analysis - Decoding pcap files + +Before analyzing files, install libpcap-dev: + + $ sudo apt install libpcap-dev + +then install pypcap: + + https://github.com/pynetwork/pypcap """ import sys @@ -15,7 +23,7 @@ except: pass -from .debugging import ModuleLogger, DebugContents, bacpypes_debugging +from .debugging import ModuleLogger, bacpypes_debugging, btox, xtob from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -33,13 +41,6 @@ socket.IPPROTO_UDP:'udp', socket.IPPROTO_ICMP:'icmp'} -# -# _hexify -# - -def _hexify(s, sep='.'): - return sep.join('%02X' % ord(c) for c in s) - # # strftimestamp # @@ -54,11 +55,11 @@ def strftimestamp(ts): @bacpypes_debugging def decode_ethernet(s): - if _debug: decode_ethernet._debug("decode_ethernet %s...", _hexify(s[:14])) + if _debug: decode_ethernet._debug("decode_ethernet %s...", btox(s[:14], '.')) d={} - d['destination_address'] = _hexify(s[0:6], ':') - d['source_address'] = _hexify(s[6:12], ':') + d['destination_address'] = btox(s[0:6], ':') + d['source_address'] = btox(s[6:12], ':') d['type'] = struct.unpack('!H',s[12:14])[0] d['data'] = s[14:] @@ -70,7 +71,7 @@ def decode_ethernet(s): @bacpypes_debugging def decode_vlan(s): - if _debug: decode_vlan._debug("decode_vlan %s...", _hexify(s[:4])) + if _debug: decode_vlan._debug("decode_vlan %s...", btox(s[:4])) d = {} x = struct.unpack('!H',s[0:2])[0] @@ -88,18 +89,18 @@ def decode_vlan(s): @bacpypes_debugging def decode_ip(s): - if _debug: decode_ip._debug("decode_ip %r", _hexify(s[:20])) + if _debug: decode_ip._debug("decode_ip %r", btox(s[:20], '.')) d = {} - d['version'] = (ord(s[0]) & 0xf0) >> 4 - d['header_len'] = ord(s[0]) & 0x0f - d['tos'] = ord(s[1]) + d['version'] = (s[0] & 0xf0) >> 4 + d['header_len'] = s[0] & 0x0f + d['tos'] = s[1] d['total_len'] = struct.unpack('!H',s[2:4])[0] d['id'] = struct.unpack('!H',s[4:6])[0] - d['flags'] = (ord(s[6]) & 0xe0) >> 5 + d['flags'] = (s[6] & 0xe0) >> 5 d['fragment_offset'] = struct.unpack('!H',s[6:8])[0] & 0x1f - d['ttl'] = ord(s[8]) - d['protocol'] = _protocols.get(ord(s[9]), '0x%.2x ?' % ord(s[9])) + d['ttl'] = s[8] + d['protocol'] = _protocols.get(s[9], '0x%.2x ?' % s[9]) d['checksum'] = struct.unpack('!H',s[10:12])[0] d['source_address'] = socket.inet_ntoa(s[12:16]) d['destination_address'] = socket.inet_ntoa(s[16:20]) @@ -117,7 +118,7 @@ def decode_ip(s): @bacpypes_debugging def decode_udp(s): - if _debug: decode_udp._debug("decode_udp %s...", _hexify(s[:8])) + if _debug: decode_udp._debug("decode_udp %s...", btox(s[:8])) d = {} d['source_port'] = struct.unpack('!H',s[0:2])[0] @@ -143,6 +144,8 @@ def decode_packet(data): # assume it is ethernet for now d = decode_ethernet(data) + pduSource = Address(d['source_address']) + pduDestination = Address(d['destination_address']) data = d['data'] # there could be a VLAN header @@ -173,10 +176,8 @@ def decode_packet(data): decode_packet._debug(" - pduDestination: %r", pduDestination) else: if _debug: decode_packet._debug(" - not a UDP packet") - return None else: if _debug: decode_packet._debug(" - not an IP packet") - return None # check for empty if not data: @@ -187,7 +188,7 @@ def decode_packet(data): pdu = PDU(data, source=pduSource, destination=pduDestination) # check for a BVLL header - if (pdu.pduData[0] == '\x81'): + if (pdu.pduData[0] == 0x81): if _debug: decode_packet._debug(" - BVLL header found") xpdu = BVLPDU() @@ -221,8 +222,8 @@ def decode_packet(data): return xpdu # check for version number - if (pdu.pduData[0] != '\x01'): - if _debug: decode_packet._debug(" - not a version 1 packet: %s...", _hexify(pdu.pduData[:30])) + if (pdu.pduData[0] != 0x01): + if _debug: decode_packet._debug(" - not a version 1 packet: %s...", btox(pdu.pduData[:30], '.')) return None # it's an NPDU @@ -355,36 +356,25 @@ def decode_file(fname): raise RuntimeError("failed to import pcap") # create a pcap object - p = pcap.pcapObject() - p.open_offline(fname) + p = pcap.pcap(fname) - i = 0 - while 1: - # the object acts like an iterator - pkt = p.next() - if not pkt: - break - - # returns a tuple - pktlen, data, timestamp = pkt + for timestamp, data in p: pkt = decode_packet(data) if not pkt: continue # save the index and timestamp in the packet - pkt._index = i + # pkt._index = i pkt._timestamp = timestamp yield pkt - i += 1 - # # Tracer # @bacpypes_debugging -class Tracer(DebugContents): +class Tracer: def __init__(self, initial_state=None): if _debug: Tracer._debug("__init__ initial_state=%r", initial_state) diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index c01e7b97..10706aa2 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -349,6 +349,7 @@ def apci_contents(self, use_dict=None, as_class=dict): # APDU # +@bacpypes_debugging class APDU(APCI, PDUData): def __init__(self, *args, **kwargs): @@ -356,12 +357,13 @@ def __init__(self, *args, **kwargs): super(APDU, self).__init__(*args, **kwargs) def encode(self, pdu): - if _debug: APCI._debug("encode %s", str(pdu)) + if _debug: APDU._debug("encode %s", str(pdu)) APCI.encode(self, pdu) pdu.put_data(self.pduData) def decode(self, pdu): - if _debug: APCI._debug("decode %s", str(pdu)) + if _debug: APDU._debug("decode %s", str(pdu)) + APCI.decode(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) @@ -393,17 +395,24 @@ def dict_contents(self, use_dict=None, as_class=dict): # between PDU's. Otherwise the APCI content would be decoded twice. # +@bacpypes_debugging class _APDU(APDU): def encode(self, pdu): + if _debug: _APDU._debug("encode %r", pdu) + APCI.update(pdu, self) pdu.put_data(self.pduData) def decode(self, pdu): + if _debug: _APDU._debug("decode %r", pdu) + APCI.update(self, pdu) self.pduData = pdu.get_data(len(pdu.pduData)) def set_context(self, context): + if _debug: _APDU._debug("set_context %r", context) + self.pduUserData = context.pduUserData self.pduDestination = context.pduSource self.pduExpectingReply = 0 diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index 9d385249..d45c212b 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -107,8 +107,12 @@ class ObjectTypesSupported(BitString): , 'positiveIntegerValue':48 , 'timePatternValue':49 , 'timeValue':50 + , 'notificationForwarder':51 + , 'alertEnrollment':52 + , 'channel':53 + , 'lightingOutput':54 } - bitLen = 51 + bitLen = 55 class ResultFlags(BitString): bitNames = \ diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 88fe28de..574901ae 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -78,7 +78,7 @@ def encode(self, taglist): """ """ if _debug: Sequence._debug("encode %r", taglist) - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -90,7 +90,7 @@ def encode(self, taglist): continue if not element.optional and value is None: raise MissingRequiredParameter("%s is a missing required element of %s" % (element.name, self.__class__.__name__)) - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # might need to encode an opening tag if element.context is not None: taglist.append(OpeningTag(element.context)) @@ -134,7 +134,10 @@ def encode(self, taglist): raise TypeError("%s must be of type %s" % (element.name, element.klass.__name__)) def decode(self, taglist): + """ + """ if _debug: Sequence._debug("decode %r", taglist) + global _sequence_of_classes, _list_of_classes # make sure we're dealing with a tag list if not isinstance(taglist, TagList): @@ -142,13 +145,14 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: if element.optional: # omitted optional element setattr(self, element.name, None) - elif element.klass in _sequence_of_classes: + elif (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # empty list setattr(self, element.name, []) else: @@ -188,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -285,7 +311,7 @@ def decode(self, taglist): raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) def debug_contents(self, indent=1, file=sys.stdout, _ids=None): - global _sequence_of_classes + global _sequence_of_classes, _list_of_classes for element in self.sequenceElements: value = getattr(self, element.name, None) @@ -295,7 +321,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): file.write("%s%s is a missing required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): file.write("%s%s\n" % (" " * indent, element.name)) helper = element.klass(value) helper.debug_contents(indent+1, file, _ids) @@ -313,6 +339,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: Sequence._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + global _sequence_of_classes, _list_of_classes # make/extend the dictionary of content if use_dict is None: @@ -324,7 +351,7 @@ def dict_contents(self, use_dict=None, as_class=dict): if value is None: continue - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): helper = element.klass(value) mapped_value = helper.dict_contents(as_class=as_class) @@ -406,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -492,6 +522,158 @@ def dict_contents(self, use_dict=None, as_class=dict): # return this new type return _SequenceOf +# +# List +# + +class List(object): + pass + +# +# ListOf +# + +_list_of_map = {} +_list_of_classes = {} + +@bacpypes_debugging +def ListOf(klass): + """Function to return a class that can encode and decode a list of + some other type.""" + if _debug: ListOf._debug("ListOf %r", klass) + + global _list_of_map + global _list_of_classes, _array_of_classes + + # if this has already been built, return the cached one + if klass in _list_of_map: + if _debug: SequenceOf._debug(" - found in cache") + return _list_of_map[klass] + + # no ListOf(ListOf(...)) allowed + if klass in _list_of_classes: + raise TypeError("nested lists disallowed") + # no ListOf(ArrayOf(...)) allowed + if klass in _array_of_classes: + raise TypeError("lists of arrays disallowed") + + # define a generic class for lists + @bacpypes_debugging + class _ListOf(List): + + subtype = None + + def __init__(self, value=None): + if _debug: _ListOf._debug("(%r)__init__ %r (subtype=%r)", self.__class__.__name__, value, self.subtype) + + if value is None: + self.value = [] + elif isinstance(value, list): + self.value = value + else: + raise TypeError("invalid constructor datatype") + + def append(self, value): + 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) + + def __len__(self): + return len(self.value) + + def __getitem__(self, item): + return self.value[item] + + def encode(self, taglist): + if _debug: _ListOf._debug("(%r)encode %r", self.__class__.__name__, taglist) + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + # a helper cooperates between the atomic value and the tag + helper = self.subtype(value) + + # build a tag and encode the data into it + tag = Tag() + helper.encode(tag) + + # now encode the tag + taglist.append(tag) + elif isinstance(value, self.subtype): + # it must have its own encoder + value.encode(taglist) + else: + raise TypeError("%s must be a %s" % (value, self.subtype.__name__)) + + def decode(self, taglist): + if _debug: _ListOf._debug("(%r)decode %r", self.__class__.__name__, taglist) + + while len(taglist) != 0: + tag = taglist.Peek() + if tag.tagClass == Tag.closingTagClass: + return + + if issubclass(self.subtype, (Atomic, AnyAtomic)): + if _debug: _ListOf._debug(" - building helper: %r %r", self.subtype, tag) + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = self.subtype(tag) + + # save the value + self.value.append(helper.value) + else: + if _debug: _ListOf._debug(" - building value: %r", self.subtype) + # build an element + value = self.subtype() + + # let it decode itself + value.decode(taglist) + + # save what was built + self.value.append(value) + + def debug_contents(self, indent=1, file=sys.stdout, _ids=None): + i = 0 + for value in self.value: + if issubclass(self.subtype, (Atomic, AnyAtomic)): + file.write("%s[%d] = %r\n" % (" " * indent, i, value)) + elif isinstance(value, self.subtype): + file.write("%s[%d]" % (" " * indent, i)) + value.debug_contents(indent+1, file, _ids) + else: + file.write("%s[%d] %s must be a %s" % (" " * indent, i, value, self.subtype.__name__)) + i += 1 + + def dict_contents(self, use_dict=None, as_class=dict): + # return sequences as arrays + mapped_value = [] + + for value in self.value: + if issubclass(self.subtype, Atomic): + mapped_value.append(value) ### ambiguous + elif issubclass(self.subtype, AnyAtomic): + mapped_value.append(value.value) ### ambiguous + elif isinstance(value, self.subtype): + mapped_value.append(value.dict_contents(as_class=as_class)) + + # return what we built + return mapped_value + + # constrain it to a list of a specific type of item + setattr(_ListOf, 'subtype', klass) + _ListOf.__name__ = 'ListOf' + klass.__name__ + if _debug: ListOf._debug(" - build this class: %r", _ListOf) + + # cache this type + _list_of_map[klass] = _ListOf + _list_of_classes[_ListOf] = 1 + + # return this new type + return _ListOf + # # Array # @@ -593,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -844,6 +1029,7 @@ def encode(self, taglist): def decode(self, taglist): if _debug: Choice._debug("(%r)decode %r", self.__class__.__name__, taglist) + global _sequence_of_classes, _list_of_classes # peek at the element tag = taglist.Peek() @@ -860,7 +1046,7 @@ def decode(self, taglist): if _debug: Choice._debug(" - checking choice: %s", element.name) # check for a sequence element - if element.klass in _sequence_of_classes: + if (element.klass in _sequence_of_classes) or (element.klass in _list_of_classes): # check for context encoding if element.context is None: raise NotImplementedError("choice of a SequenceOf must be context encoded") @@ -1045,9 +1231,10 @@ def cast_in(self, element): def cast_out(self, klass): """Interpret the content as a particular class.""" if _debug: Any._debug("cast_out %r", klass) + global _sequence_of_classes, _list_of_classes # check for a sequence element - if klass in _sequence_of_classes: + if (klass in _sequence_of_classes) or (klass in _list_of_classes): # build a sequence helper helper = klass() @@ -1184,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py34/bacpypes/core.py b/py34/bacpypes/core.py index b2c46e34..6430b864 100755 --- a/py34/bacpypes/core.py +++ b/py34/bacpypes/core.py @@ -52,10 +52,10 @@ def stop(*args): # @bacpypes_debugging -def dump_stack(): - if _debug: dump_stack._debug("dump_stack") +def dump_stack(debug_handler): + if _debug: dump_stack._debug("dump_stack %r", debug_handler) for filename, lineno, fn, _ in traceback.extract_stack()[:-1]: - sys.stderr.write(" %-20s %s:%s\n" % (fn, filename.split('/')[-1], lineno)) + debug_handler(" %-20s %s:%s", fn, filename.split('/')[-1], lineno) # # print_stack diff --git a/py34/bacpypes/local/__init__.py b/py34/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py34/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py new file mode 100644 index 00000000..c823b897 --- /dev/null +++ b/py34/bacpypes/local/device.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, int): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + diff --git a/py34/bacpypes/local/file.py b/py34/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py34/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py34/bacpypes/local/object.py b/py34/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py34/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py34/bacpypes/local/schedule.py b/py34/bacpypes/local/schedule.py new file mode 100644 index 00000000..d22911a3 --- /dev/null +++ b/py34/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [c for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py34/bacpypes/netservice.py b/py34/bacpypes/netservice.py index 6d02f687..391abea7 100755 --- a/py34/bacpypes/netservice.py +++ b/py34/bacpypes/netservice.py @@ -4,7 +4,7 @@ Network Service """ -from copy import copy as _copy +from copy import deepcopy as _deepcopy from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError @@ -27,32 +27,134 @@ ROUTER_UNREACHABLE = 3 # cannot route # -# NetworkReference +# RouterInfo # -class NetworkReference: - """These objects map a network to a router.""" +class RouterInfo(DebugContents): + """These objects are routing information records that map router + addresses with destination networks.""" - def __init__(self, net, router, status): - self.network = net - self.router = router - self.status = status + _debug_contents = ('snet', 'address', 'dnets', 'status') + + def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + self.snet = snet # source network + self.address = address # address of the router + self.dnets = dnets # list of reachable networks through this router + self.status = status # router status # -# RouterReference +# RouterInfoCache # -class RouterReference(DebugContents): - """These objects map a router; the adapter to talk to it, - its address, and a list of networks that it routes to.""" +@bacpypes_debugging +class RouterInfoCache: + + def __init__(self): + if _debug: RouterInfoCache._debug("__init__") + + self.routers = {} # (snet, address) -> RouterInfo + self.networks = {} # network -> RouterInfo + + def get_router_info(self, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r", dnet) + + # check to see if we know about it + if dnet not in self.networks: + if _debug: RouterInfoCache._debug(" - no route") + return None + + # return the network and address + router_info = self.networks[dnet] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # return the network, address, and status + return (router_info.snet, router_info.address, router_info.status) - _debug_contents = ('adapter-', 'address', 'networks', 'status') + def update_router_info(self, snet, address, dnets): + if _debug: RouterInfoCache._debug("update_router_info %r %r %r", snet, address, dnets) + + # look up the router reference, make a new record if necessary + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - new router") + router_info = self.routers[key] = RouterInfo(snet, address, list()) + else: + router_info = self.routers[key] + + # add (or move) the destination networks + for dnet in dnets: + if dnet in self.networks: + other_router = self.networks[dnet] + if other_router is router_info: + if _debug: RouterInfoCache._debug(" - existing router, match") + continue + elif dnet not in other_router.dnets: + if _debug: RouterInfoCache._debug(" - where did it go?") + else: + other_router.dnets.remove(dnet) + if not other_router.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[(snet, other_router.address)] + + # add a reference to the router + self.networks[dnet] = router_info + if _debug: RouterInfoCache._debug(" - reference added") + + # maybe update the list of networks for this router + if dnet not in router_info.dnets: + router_info.dnets.append(dnet) + if _debug: RouterInfoCache._debug(" - dnet added, now: %r", router_info.dnets) + + def update_router_status(self, snet, address, status): + if _debug: RouterInfoCache._debug("update_router_status %r %r %r", snet, address, status) + + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - not a router we care about") + return - def __init__(self, adapter, addr, nets, status): - self.adapter = adapter - self.address = addr # local station relative to the adapter - self.networks = nets # list of remote networks - self.status = status # status as presented by the router + router_info = self.routers[key] + router_info.status = status + if _debug: RouterInfoCache._debug(" - status updated") + + def delete_router_info(self, snet, address=None, dnets=None): + if _debug: RouterInfoCache._debug("delete_router_info %r %r %r", dnets) + + # if address is None, remove all the routers for the network + if address is None: + for rnet, raddress in self.routers.keys(): + if snet == rnet: + if _debug: RouterInfoCache._debug(" - going down") + self.delete_router_info(snet, raddress) + if _debug: RouterInfoCache._debug(" - back topside") + return + + # look up the router reference + key = (snet, address) + if key not in self.routers: + if _debug: RouterInfoCache._debug(" - unknown router") + return + + router_info = self.routers[key] + if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + + # if dnets is None, remove all the networks for the router + if dnets is None: + dnets = router_info.dnets + + # loop through the list of networks to be deleted + for dnet in dnets: + if dnet in self.networks: + del self.networks[dnet] + if _debug: RouterInfoCache._debug(" - removed from networks: %r", dnet) + if dnet in router_info.dnets: + router_info.dnets.remove(dnet) + if _debug: RouterInfoCache._debug(" - removed from router_info: %r", dnet) + + # see if we still care + if not router_info.dnets: + if _debug: RouterInfoCache._debug(" - no longer care about this router") + del self.routers[key] # # NetworkAdapter @@ -64,14 +166,11 @@ class NetworkAdapter(Client, DebugContents): _debug_contents = ('adapterSAP-', 'adapterNet') def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %r (net=%r) cid=%r", sap, net, cid) + if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net - # add this to the list of adapters for the network - sap.adapters.append(self) - 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) @@ -105,117 +204,73 @@ class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): , 'localAdapter-', 'localAddress' ) - def __init__(self, sap=None, sid=None): + def __init__(self, routerInfoCache=None, sap=None, sid=None): if _debug: NetworkServiceAccessPoint._debug("__init__ sap=%r sid=%r", sap, sid) ServiceAccessPoint.__init__(self, sap) Server.__init__(self, sid) - self.adapters = [] # list of adapters - self.routers = {} # (adapter, address) -> RouterReference - self.networks = {} # network -> RouterReference + # map of directly connected networks + self.adapters = {} # net -> NetworkAdapter - self.localAdapter = None # which one is local - self.localAddress = None # what is the local address + # use the provided cache or make a default one + self.router_info_cache = routerInfoCache or RouterInfoCache() + + # map to a list of application layer packets waiting for a path + self.pending_nets = {} + + # these are set when bind() is called + self.local_adapter = None + self.local_address = None def bind(self, server, net=None, address=None): """Create a network adapter object and bind.""" if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) - if (net is None) and self.adapters: + # make sure this hasn't already been called with this network + if net in self.adapters: raise RuntimeError("already bound") - # create an adapter object + # 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: - self.localAdapter = adapter - self.localAddress = address + self.local_adapter = adapter + self.local_address = address # bind to the server bind(adapter, server) #----- - def add_router_references(self, adapter, address, netlist): + def add_router_references(self, snet, address, dnets): """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", adapter, address, netlist) + if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) - # make a key for the router reference - rkey = (adapter, address) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - for snet in netlist: - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] + # pass this along to the cache + self.router_info_cache.update_router_info(snet, address, dnets) - if rref.adapter == adapter and rref.address == address: - pass # matches current entry - else: - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] - - # remove the network - del self.networks[snet] - - ### check to see if it is OK to add the new entry + def delete_router_references(self, snet, address=None, dnets=None): + """Delete references to routers/networks.""" + if _debug: NetworkServiceAccessPoint._debug("delete_router_references %r %r %r", snet, address, dnets) - # get the router reference for this router - rref = self.routers.get(rkey, None) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if we have an adapter for the snet + if snet not in self.adapters: + raise RuntimeError("no adapter for network: %d" % (snet,)) - # reference the snet - self.networks[snet] = rref - else: - # new reference - rref = RouterReference( adapter, address, [snet], 0) - self.routers[rkey] = rref - - # reference the snet - self.networks[snet] = rref - - def remove_router_references(self, adapter, address=None): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("remove_router_references %r %r", adapter, address) - - delrlist = [] - delnlist = [] - # scan through the dictionary of router references - for rkey in self.routers.keys(): - # rip apart the key - radapter, raddress = rkey - - # pick all references on the adapter, optionally limited to a specific address - match = radapter is adapter - if match and address is not None: - match = (raddress == address) - if not match: - continue - - # save it for deletion - delrlist.append(rkey) - delnlist.extend(self.routers[rkey].networks) - if _debug: - NetworkServiceAccessPoint._debug(" - delrlist: %r", delrlist) - NetworkServiceAccessPoint._debug(" - delnlist: %r", delnlist) - - # delete the entries - for rkey in delrlist: - try: - del self.routers[rkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - rkey not in self.routers: %r", rkey) - for nkey in delnlist: - try: - del self.networks[nkey] - except KeyError: - if _debug: NetworkServiceAccessPoint._debug(" - nkey not in self.networks: %r", rkey) + # pass this along to the cache + self.router_info_cache.delete_router_info(snet, address, dnets) #----- @@ -227,11 +282,12 @@ def indication(self, pdu): raise ConfigurationError("no adapters") # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.localAdapter): + if (len(self.adapters) > 1) and (not self.local_adapter): raise ConfigurationError("local adapter must be set") # get the local adapter - adapter = self.localAdapter or self.adapters[0] + adapter = self.local_adapter or self.adapters[None] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -263,7 +319,7 @@ def indication(self, pdu): npdu.npduDADR = apdu.pduDestination # send it to all of connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): xadapter.process_npdu(npdu) return @@ -279,32 +335,53 @@ def indication(self, pdu): ### when it's a directly connected network raise RuntimeError("addressing problem") - # check for an available path - if dnet in self.networks: - rref = self.networks[dnet] - adapter = rref.adapter + # get it ready to send when the path is found + npdu.pduDestination = None + npdu.npduDADR = apdu.pduDestination + + # we might already be waiting for a path for this network + if dnet in self.pending_nets: + if _debug: NetworkServiceAccessPoint._debug(" - already waiting for path") + self.pending_nets[dnet].append(npdu) + return - ### make sure the direct connect is OK, may need to connect + # check cache for an available path + path_info = self.router_info_cache.get_router_info(dnet) - ### make sure the peer router is OK, may need to connect + # if there is info, we have a path + if path_info: + snet, address, status = path_info + if _debug: NetworkServiceAccessPoint._debug(" - path found: %r, %r, %r", snet, address, status) + + # check for an adapter + if snet not in self.adapters: + raise RuntimeError("network found but not connected: %r", snet) + adapter = self.adapters[snet] + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) # fix the destination - npdu.pduDestination = rref.address - npdu.npduDADR = apdu.pduDestination + npdu.pduDestination = address # send it along adapter.process_npdu(npdu) return - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network, broadcast to discover it") + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # set the destination - npdu.pduDestination = LocalBroadcast() - npdu.npduDADR = apdu.pduDestination + # add it to the list of packets waiting for the network + net_list = self.pending_nets.get(dnet, None) + if net_list is None: + net_list = self.pending_nets[dnet] = [] + net_list.append(npdu) + + # build a request for the network and send it to all of the adapters + xnpdu = WhoIsRouterToNetwork(dnet) + xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: - xadapter.process_npdu(npdu) + for adapter in self.adapters.values(): + ### make sure the adapter is OK + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) @@ -312,83 +389,68 @@ def process_npdu(self, adapter, npdu): # make sure our configuration is OK if (not self.adapters): raise ConfigurationError("no adapters") - if (len(self.adapters) > 1) and (not self.localAdapter): - raise ConfigurationError("local adapter must be set") # check for source routing if npdu.npduSADR and (npdu.npduSADR.addrType != Address.nullAddr): + if _debug: NetworkServiceAccessPoint._debug(" - check source path") + # see if this is attempting to spoof a directly connected network snet = npdu.npduSADR.addrNet - for xadapter in self.adapters: - if (xadapter is not adapter) and (snet == xadapter.adapterNet): - NetworkServiceAccessPoint._warning("spoof?") - ### log this - return - - # make a key for the router reference - rkey = (adapter, npdu.pduSource) - - # see if this is spoofing an existing routing table entry - if snet in self.networks: - rref = self.networks[snet] - if rref.adapter == adapter and rref.address == npdu.pduSource: - pass # matches current entry - else: - if _debug: NetworkServiceAccessPoint._debug(" - replaces entry") - - ### check to see if this source could be a router to the new network - - # remove the network from the rref - i = rref.networks.index(snet) - del rref.networks[i] + if snet in self.adapters: + NetworkServiceAccessPoint._warning(" - path error (1)") + return - # remove the network - del self.networks[snet] + # see if there is routing information for this source network + router_info = self.router_info_cache.get_router_info(snet) + if router_info: + router_snet, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug(" - router_address, router_status: %r, %r", router_address, router_status) - # get the router reference for this router - rref = self.routers.get(rkey) - if rref: - if snet not in rref.networks: - # add the network - rref.networks.append(snet) + # see if the router has changed + if not (router_address == npdu.pduSource): + if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) else: - # new reference - rref = RouterReference( adapter, npdu.pduSource, [snet], 0) - self.routers[rkey] = rref + if _debug: NetworkServiceAccessPoint._debug(" - new path") - # reference the snet - self.networks[snet] = rref + # pass this new path along to the cache + self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) # check for destination routing if (not npdu.npduDADR) or (npdu.npduDADR.addrType == Address.nullAddr): - processLocally = (not self.localAdapter) or (adapter is self.localAdapter) or (npdu.npduNetMessage is not None) + if _debug: NetworkServiceAccessPoint._debug(" - no DADR") + + processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote broadcast") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: - if not self.localAdapter: - return + if _debug: NetworkServiceAccessPoint._debug(" - DADR is remote station") + if (npdu.npduDADR.addrNet == adapter.adapterNet): - ### log this, attempt to route to a network the device is already on + NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = (npdu.npduDADR.addrNet == self.localAdapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.localAddress.addrAddr) + processLocally = self.local_adapter \ + and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - DADR is global broadcast") + processLocally = True forwardMessage = True @@ -402,14 +464,18 @@ def process_npdu(self, adapter, npdu): # application or network layer message if npdu.npduNetMessage is None: + if _debug: NetworkServiceAccessPoint._debug(" - application layer message") + if processLocally and self.serverPeer: + if _debug: NetworkServiceAccessPoint._debug(" - processing APDU locally") + # decode as a generic APDU apdu = _APDU(user_data=npdu.pduUserData) - apdu.decode(_copy(npdu)) + apdu.decode(_deepcopy(npdu)) if _debug: NetworkServiceAccessPoint._debug(" - apdu: %r", apdu) # see if it needs to look routed - if (len(self.adapters) > 1) and (adapter != self.localAdapter): + if (len(self.adapters) > 1) and (adapter != self.local_adapter): # combine the source address if not npdu.npduSADR: apdu.pduSource = RemoteStation( adapter.adapterNet, npdu.pduSource.addrAddr ) @@ -418,7 +484,7 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.localAddress + apdu.pduDestination = self.local_address elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -444,34 +510,40 @@ def process_npdu(self, adapter, npdu): # pass upstream to the application layer self.response(apdu) - if not forwardMessage: - return else: + if _debug: NetworkServiceAccessPoint._debug(" - network layer message") + if processLocally: if npdu.npduNetMessage not in npdu_types: if _debug: NetworkServiceAccessPoint._debug(" - unknown npdu type: %r", npdu.npduNetMessage) return + if _debug: NetworkServiceAccessPoint._debug(" - processing NPDU locally") + # do a deeper decode of the NPDU xpdu = npdu_types[npdu.npduNetMessage](user_data=npdu.pduUserData) - xpdu.decode(_copy(npdu)) + xpdu.decode(_deepcopy(npdu)) # pass to the service element self.sap_request(adapter, xpdu) - if not forwardMessage: - return + # might not need to forward this to other devices + if not forwardMessage: + if _debug: NetworkServiceAccessPoint._debug(" - no forwarding") + return # make sure we're really a router if (len(self.adapters) == 1): + if _debug: NetworkServiceAccessPoint._debug(" - not a router") return # make sure it hasn't looped if (npdu.npduHopCount == 0): + if _debug: NetworkServiceAccessPoint._debug(" - no more hops") return # build a new NPDU to send to other adapters - newpdu = _copy(npdu) + newpdu = _deepcopy(npdu) # clear out the source and destination newpdu.pduSource = None @@ -488,48 +560,65 @@ def process_npdu(self, adapter, npdu): # if this is a broadcast it goes everywhere if npdu.npduDADR.addrType == Address.globalBroadcastAddr: + if _debug: NetworkServiceAccessPoint._debug(" - global broadcasting") newpdu.pduDestination = LocalBroadcast() - for xadapter in self.adapters: + for xadapter in self.adapters.values(): if (xadapter is not adapter): - xadapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr) \ or (npdu.npduDADR.addrType == Address.remoteStationAddr): dnet = npdu.npduDADR.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - remote station/broadcast") - # see if this should go to one of our directly connected adapters - for xadapter in self.adapters: - if dnet == xadapter.adapterNet: - if _debug: NetworkServiceAccessPoint._debug(" - found direct connect via %r", xadapter) - if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): - newpdu.pduDestination = LocalBroadcast() - else: - newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) + # see if this a locally connected network + if dnet in self.adapters: + xadapter = self.adapters[dnet] + if xadapter is adapter: + if _debug: NetworkServiceAccessPoint._debug(" - path error (4)") + return + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - # last leg in routing - newpdu.npduDADR = None + # if this was a remote broadcast, it's now a local one + if (npdu.npduDADR.addrType == Address.remoteBroadcastAddr): + newpdu.pduDestination = LocalBroadcast() + else: + newpdu.pduDestination = LocalStation(npdu.npduDADR.addrAddr) - # send the packet downstream - xadapter.process_npdu(newpdu) - return + # last leg in routing + newpdu.npduDADR = None - # see if we know how to get there - if dnet in self.networks: - rref = self.networks[dnet] - newpdu.pduDestination = rref.address + # send the packet downstream + xadapter.process_npdu(_deepcopy(newpdu)) + return - ### check to make sure the router is OK + # see if there is routing information for this destination network + router_info = self.router_info_cache.get_router_info(dnet) + if router_info: + router_net, router_address, router_status = router_info + if _debug: NetworkServiceAccessPoint._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + + if router_net not in self.adapters: + if _debug: NetworkServiceAccessPoint._debug(" - path error (5)") + return - ### check to make sure the network is OK, may need to connect + xadapter = self.adapters[router_net] + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) - if _debug: NetworkServiceAccessPoint._debug(" - newpdu: %r", newpdu) + # the destination is the address of the router + newpdu.pduDestination = router_address # send the packet downstream - rref.adapter.process_npdu(newpdu) + xadapter.process_npdu(_deepcopy(newpdu)) return + if _debug: NetworkServiceAccessPoint._debug(" - no router info found") + ### queue this message for reprocessing when the response comes back # try to find a path to the network @@ -537,16 +626,17 @@ def process_npdu(self, adapter, npdu): xnpdu.pduDestination = LocalBroadcast() # send it to all of the connected adapters - for xadapter in self.adapters: + for xadapter in self.adapters.values(): # skip the horse it rode in on if (xadapter is adapter): continue - ### make sure the adapter is OK + # pass this along as if it came from the NSE self.sap_indication(xadapter, xnpdu) - ### log this, what to do? - return + return + + if _debug: NetworkServiceAccessPoint._debug(" - bad DADR: %r", npdu.npduDADR) def sap_indication(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("sap_indication %r %r", adapter, npdu) @@ -618,17 +708,15 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # build a list of reachable networks netlist = [] - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter): - netlist.append(xadapter.adapterNet) + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is adapter): + continue + + # add the direct network + netlist.append(xadapter.adapterNet) - # build a list of other available networks - for net, rref in sap.networks.items(): - if rref.adapter is not adapter: - ### skip those marked unreachable - ### skip those that are not available - netlist.append(net) + ### add the other reachable if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -643,42 +731,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): else: # requesting a specific network if _debug: NetworkServiceElement._debug(" - requesting specific network: %r", npdu.wirtnNetwork) + dnet = npdu.wirtnNetwork - # start with directly connected networks - for xadapter in sap.adapters: - if (xadapter is not adapter) and (npdu.wirtnNetwork == xadapter.adapterNet): - if _debug: NetworkServiceElement._debug(" - found it directly connected") + # check the directly connected networks + if dnet in sap.adapters: + if _debug: NetworkServiceElement._debug(" - directly connected") - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) - break else: - # check for networks I know about - if npdu.wirtnNetwork in sap.networks: - rref = sap.networks[npdu.wirtnNetwork] - if rref.adapter is adapter: - if _debug: NetworkServiceElement._debug(" - same net as request") - - else: - if _debug: NetworkServiceElement._debug(" - found on adapter: %r", rref.adapter) + # see if there is routing information for this source network + router_info = sap.router_info_cache.get_router_info(dnet) + if router_info: + if _debug: NetworkServiceElement._debug(" - router found") + + router_net, router_address, router_status = router_info + if _debug: NetworkServiceElement._debug( + " - router_net, router_address, router_status: %r, %r, %r", + router_net, router_address, router_status, + ) + if router_net not in sap.adapters: + if _debug: NetworkServiceElement._debug(" - path error (6)") + return - # build a response - iamrtn = IAmRouterToNetwork([npdu.wirtnNetwork], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it back - self.response(adapter, iamrtn) + # send it back + self.response(adapter, iamrtn) else: if _debug: NetworkServiceElement._debug(" - forwarding request to other adapters") # build a request - whoisrtn = WhoIsRouterToNetwork(npdu.wirtnNetwork, user_data=npdu.pduUserData) + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) whoisrtn.pduDestination = LocalBroadcast() # if the request had a source, forward it along @@ -689,7 +781,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) # send it to all of the (other) adapters - for xadapter in sap.adapters: + for xadapter in sap.adapters.values(): if xadapter is not adapter: if _debug: NetworkServiceElement._debug(" - sending on adapter: %r", xadapter) self.request(xadapter, whoisrtn) @@ -697,8 +789,46 @@ def WhoIsRouterToNetwork(self, adapter, npdu): def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + # pass along to the service access point - self.elementService.add_router_references(adapter, npdu.pduSource, npdu.iartnNetworkList) + sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + + # skip if this is not a router + if len(sap.adapters) > 1: + # build a broadcast annoucement + iamrtn = IAmRouterToNetwork(npdu.iartnNetworkList, user_data=npdu.pduUserData) + iamrtn.pduDestination = LocalBroadcast() + + # send it to all of the connected adapters + for xadapter in sap.adapters.values(): + # skip the horse it rode in on + if (xadapter is adapter): + continue + + # request this + self.request(xadapter, iamrtn) + + # look for pending NPDUs for the networks + for dnet in npdu.iartnNetworkList: + pending_npdus = sap.pending_nets.get(dnet, None) + if pending_npdus is not None: + if _debug: NetworkServiceElement._debug(" - %d pending to %r", len(pending_npdus), dnet) + + # delete the references + del sap.pending_nets[dnet] + + # now reprocess them + for pending_npdu in pending_npdus: + if _debug: NetworkServiceElement._debug(" - sending %s", repr(pending_npdu)) + + # the destination is the address of the router + pending_npdu.pduDestination = npdu.pduSource + + # send the packet downstream + adapter.process_npdu(pending_npdu) def ICouldBeRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("ICouldBeRouterToNetwork %r %r", adapter, npdu) diff --git a/py34/bacpypes/npdu.py b/py34/bacpypes/npdu.py index c6ad9d97..436f6bb8 100755 --- a/py34/bacpypes/npdu.py +++ b/py34/bacpypes/npdu.py @@ -476,7 +476,7 @@ def __init__(self, netList=[], *args, **kwargs): def encode(self, npdu): NPCI.update(npdu, self) - for net in self.ratnNetworkList: + for net in self.rbtnNetworkList: npdu.put_short(net) def decode(self, npdu): @@ -543,6 +543,12 @@ def __init__(self, dnet=None, portID=None, portInfo=None): self.rtPortID = portID self.rtPortInfo = portInfo + def __eq__(self, other): + """Return true iff entries are identical.""" + return (self.rtDNET == other.rtDNET) and \ + (self.rtPortID == other.rtPortID) and \ + (self.rtPortInfo == other.rtPortInfo) + def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" # make/extend the dictionary of content @@ -735,6 +741,11 @@ class WhatIsNetworkNumber(NPDU): messageType = 0x12 + def __init__(self, *args, **kwargs): + super(WhatIsNetworkNumber, self).__init__(*args, **kwargs) + + self.npduNetMessage = WhatIsNetworkNumber.messageType + def encode(self, npdu): NPCI.update(npdu, self) @@ -755,25 +766,32 @@ def npdu_contents(self, use_dict=None, as_class=dict): class NetworkNumberIs(NPDU): - _debug_contents = ('nniNET', 'nniFlag',) + _debug_contents = ('nniNet', 'nniFlag',) messageType = 0x13 + def __init__(self, net=None, flag=None, *args, **kwargs): + super(NetworkNumberIs, self).__init__(*args, **kwargs) + + self.npduNetMessage = NetworkNumberIs.messageType + self.nniNet = net + self.nniFlag = flag + def encode(self, npdu): NPCI.update(npdu, self) - npdu.put_short( self.nniNET ) + npdu.put_short( self.nniNet ) npdu.put( self.nniFlag ) def decode(self, npdu): NPCI.update(self, npdu) - self.nniNET = npdu.get_short() + self.nniNet = npdu.get_short() self.nniFlag = npdu.get() def npdu_contents(self, use_dict=None, as_class=dict): return key_value_contents(use_dict=use_dict, as_class=as_class, key_values=( ('function', 'NetorkNumberIs'), - ('net', self.nniNET), + ('net', self.nniNet), ('flag', self.nniFlag), )) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 9f31dd90..97b58992 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -15,8 +15,8 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ Unsigned -from .constructeddata import AnyAtomic, Array, ArrayOf, Choice, Element, \ - Sequence, SequenceOf +from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ + Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ AccessEvent, AccessPassbackMode, AccessRule, AccessThreatLevel, \ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -155,7 +156,12 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr # keep the arguments self.identifier = identifier + + # check the datatype self.datatype = datatype + if not issubclass(datatype, (Atomic, Sequence, Choice, Array, List, AnyAtomic)): + raise TypeError("invalid datatype for property: %s" % (identifier,)) + self.optional = optional self.mutable = mutable self.default = default @@ -210,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): @@ -256,6 +269,38 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False # value is mutated into a new array value = self.datatype(value) + # if it's an array, make sure it's valid regarding arrayIndex provided + elif issubclass(self.datatype, List): + if _debug: Property._debug(" - property is list, checking subtype") + + # changing a single element + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # replacing the array + if not isinstance(value, list): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # check validity regarding subtype + for item in value: + # if it's atomic, make sure it's valid + if issubclass(self.datatype.subtype, Atomic): + if _debug: Property._debug(" - subtype is atomic, checking value") + if not self.datatype.subtype.is_valid(item): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__, + )) + # constructed type + elif not isinstance(item, self.datatype.subtype): + raise InvalidParameterDatatype("elements of %s must be of type %s" % ( + self.identifier, self.datatype.subtype.__name__ + )) + + # value is mutated into a new list + value = self.datatype(value) + # some kind of constructed data elif not isinstance(value, self.datatype): if _debug: Property._debug(" - property is not atomic and wrong type") @@ -657,7 +702,7 @@ class AccessCredentialObject(Object): , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('credentialStatus', BinaryPV) - , ReadableProperty('reasonForDisable', SequenceOf(AccessCredentialDisableReason)) + , ReadableProperty('reasonForDisable', ListOf(AccessCredentialDisableReason)) , ReadableProperty('authenticationFactors', ArrayOf(CredentialAuthenticationFactor)) , ReadableProperty('activationTime', DateTime) , ReadableProperty('expiryTime', DateTime) @@ -673,7 +718,7 @@ class AccessCredentialObject(Object): , OptionalProperty('traceFlag', Boolean) , OptionalProperty('threatAuthority', AccessThreatLevel) , OptionalProperty('extendedTimeEnable', Boolean) - , OptionalProperty('authorizationExemptions', SequenceOf(AuthorizationException)) + , OptionalProperty('authorizationExemptions', ListOf(AuthorizationException)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) # , OptionalProperty('masterExemption', Boolean) # , OptionalProperty('passbackExemption', Boolean) @@ -700,12 +745,12 @@ class AccessDoorObject(Object): , OptionalProperty('doorUnlockDelayTime', Unsigned) , ReadableProperty('doorOpenTooLongTime', Unsigned) , OptionalProperty('doorAlarmState', DoorAlarmState) - , OptionalProperty('maskedAlarmValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('maskedAlarmValues', ListOf(DoorAlarmState)) , OptionalProperty('maintenanceRequired', Maintenance) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(DoorAlarmState)) - , OptionalProperty('faultValues', SequenceOf(DoorAlarmState)) + , OptionalProperty('alarmValues', ListOf(DoorAlarmState)) + , OptionalProperty('faultValues', ListOf(DoorAlarmState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -735,7 +780,7 @@ class AccessPointObject(Object): , OptionalProperty('lockout', Boolean) , OptionalProperty('lockoutRelinquishTime', Unsigned) , OptionalProperty('failedAttempts', Unsigned) - , OptionalProperty('failedAttemptEvents', SequenceOf(AccessEvent)) + , OptionalProperty('failedAttemptEvents', ListOf(AccessEvent)) , OptionalProperty('maxFailedAttempts', Unsigned) , OptionalProperty('failedAttemptsTime', Unsigned) , OptionalProperty('threatLevel', AccessThreatLevel) @@ -755,8 +800,8 @@ class AccessPointObject(Object): , OptionalProperty('zoneFrom', DeviceObjectReference) , OptionalProperty('notificationClass', Unsigned) , OptionalProperty('transactionNotificationClass', Unsigned) - , OptionalProperty('accessAlarmEvents', SequenceOf(AccessEvent)) - , OptionalProperty('accessTransactionEvents', SequenceOf(AccessEvent)) + , OptionalProperty('accessAlarmEvents', ListOf(AccessEvent)) + , OptionalProperty('accessTransactionEvents', ListOf(AccessEvent)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -794,9 +839,9 @@ class AccessUserObject(Object): , OptionalProperty('userName', CharacterString) , OptionalProperty('userExternalIdentifier', CharacterString) , OptionalProperty('userInformationReference', CharacterString) - , OptionalProperty('members', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) - , ReadableProperty('credentials', SequenceOf(DeviceObjectReference)) + , OptionalProperty('members', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) + , ReadableProperty('credentials', ListOf(DeviceObjectReference)) ] @register_object_type @@ -814,18 +859,18 @@ class AccessZoneObject(Object): , OptionalProperty('adjustValue', Integer) , OptionalProperty('occupancyUpperLimit', Unsigned) , OptionalProperty('occupancyLowerLimit', Unsigned) - , OptionalProperty('credentialsInZone', SequenceOf(DeviceObjectReference) ) + , OptionalProperty('credentialsInZone', ListOf(DeviceObjectReference) ) , OptionalProperty('lastCredentialAdded', DeviceObjectReference) , OptionalProperty('lastCredentialAddedTime', DateTime) , OptionalProperty('lastCredentialRemoved', DeviceObjectReference) , OptionalProperty('lastCredentialRemovedTime', DateTime) , OptionalProperty('passbackMode', AccessPassbackMode) , OptionalProperty('passbackTimeout', Unsigned) - , ReadableProperty('entryPoints', SequenceOf(DeviceObjectReference)) - , ReadableProperty('exitPoints', SequenceOf(DeviceObjectReference)) + , ReadableProperty('entryPoints', ListOf(DeviceObjectReference)) + , ReadableProperty('exitPoints', ListOf(DeviceObjectReference)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(AccessZoneOccupancyState)) + , OptionalProperty('alarmValues', ListOf(AccessZoneOccupancyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1161,7 +1206,7 @@ class CalendarObject(Object): objectType = 'calendar' properties = \ [ ReadableProperty('presentValue', Boolean) - , ReadableProperty('dateList', SequenceOf(CalendarEntry)) + , ReadableProperty('dateList', ListOf(CalendarEntry)) ] @register_object_type @@ -1325,8 +1370,8 @@ class DeviceObject(Object): , OptionalProperty('structuredObjectList', ArrayOf(ObjectIdentifier)) , ReadableProperty('maxApduLengthAccepted', Unsigned) , ReadableProperty('segmentationSupported', Segmentation) - , OptionalProperty('vtClassesSupported', SequenceOf(VTClass)) - , OptionalProperty('activeVtSessions', SequenceOf(VTSession)) + , OptionalProperty('vtClassesSupported', ListOf(VTClass)) + , OptionalProperty('activeVtSessions', ListOf(VTSession)) , OptionalProperty('localTime', Time) , OptionalProperty('localDate', Date) , OptionalProperty('utcOffset', Integer) @@ -1334,10 +1379,10 @@ class DeviceObject(Object): , OptionalProperty('apduSegmentTimeout', Unsigned) , ReadableProperty('apduTimeout', Unsigned) , ReadableProperty('numberOfApduRetries', Unsigned) - , OptionalProperty('timeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('timeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('maxMaster', Unsigned) , OptionalProperty('maxInfoFrames', Unsigned) - , ReadableProperty('deviceAddressBinding', SequenceOf(AddressBinding)) + , ReadableProperty('deviceAddressBinding', ListOf(AddressBinding)) , ReadableProperty('databaseRevision', Unsigned) , OptionalProperty('configurationFiles', ArrayOf(ObjectIdentifier)) , OptionalProperty('lastRestoreTime', TimeStamp) @@ -1346,16 +1391,16 @@ class DeviceObject(Object): , OptionalProperty('restorePreparationTime', Unsigned) , OptionalProperty('restoreCompletionTime', Unsigned) , OptionalProperty('backupAndRestoreState', BackupState) - , OptionalProperty('activeCovSubscriptions', SequenceOf(COVSubscription)) + , OptionalProperty('activeCovSubscriptions', ListOf(COVSubscription)) , OptionalProperty('maxSegmentsAccepted', Unsigned) , OptionalProperty('slaveProxyEnable', ArrayOf(Boolean)) , OptionalProperty('autoSlaveDiscovery', ArrayOf(Boolean)) - , OptionalProperty('slaveAddressBinding', SequenceOf(AddressBinding)) - , OptionalProperty('manualSlaveAddressBinding', SequenceOf(AddressBinding)) + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) , OptionalProperty('lastRestartReason', RestartReason) , OptionalProperty('timeOfDeviceRestart', TimeStamp) - , OptionalProperty('restartNotificationRecipients', SequenceOf(Recipient)) - , OptionalProperty('utcTimeSynchronizationRecipients', SequenceOf(Recipient)) + , OptionalProperty('restartNotificationRecipients', ListOf(Recipient)) + , OptionalProperty('utcTimeSynchronizationRecipients', ListOf(Recipient)) , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) @@ -1415,7 +1460,7 @@ class EventLogObject(Object): , OptionalProperty('stopTime', DateTime) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(EventLogRecord)) + , ReadableProperty('logBuffer', ListOf(EventLogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) @@ -1479,7 +1524,7 @@ class GlobalGroupObject(Object): , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) , OptionalProperty('covuPeriod', Unsigned) - , OptionalProperty('covuRecipients', SequenceOf(Recipient)) + , OptionalProperty('covuRecipients', ListOf(Recipient)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) ] @@ -1487,8 +1532,8 @@ class GlobalGroupObject(Object): class GroupObject(Object): objectType = 'group' properties = \ - [ ReadableProperty('listOfGroupMembers', SequenceOf(ReadAccessSpecification)) - , ReadableProperty('presentValue', SequenceOf(ReadAccessResult)) + [ ReadableProperty('listOfGroupMembers', ListOf(ReadAccessSpecification)) + , ReadableProperty('presentValue', ListOf(ReadAccessResult)) ] @register_object_type @@ -1573,12 +1618,12 @@ class LifeSafetyPointObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1596,7 +1641,7 @@ class LifeSafetyPointObject(Object): , OptionalProperty('setting', Unsigned) , OptionalProperty('directReading', Real) , OptionalProperty('units', EngineeringUnits) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] @register_object_type @@ -1611,12 +1656,12 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) , WritableProperty('mode', LifeSafetyMode) - , ReadableProperty('acceptedModes', SequenceOf(LifeSafetyMode)) + , ReadableProperty('acceptedModes', ListOf(LifeSafetyMode)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('lifeSafetyAlarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('alarmValues', SequenceOf(LifeSafetyState)) - , OptionalProperty('faultValues', SequenceOf(LifeSafetyState)) + , OptionalProperty('lifeSafetyAlarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('alarmValues', ListOf(LifeSafetyState)) + , OptionalProperty('faultValues', ListOf(LifeSafetyState)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1631,8 +1676,8 @@ class LifeSafetyZoneObject(Object): , ReadableProperty('silenced', SilencedState) , ReadableProperty('operationExpected', LifeSafetyOperation) , OptionalProperty('maintenanceRequired', Boolean) - , ReadableProperty('zoneMembers', SequenceOf(DeviceObjectReference)) - , OptionalProperty('memberOf', SequenceOf(DeviceObjectReference)) + , ReadableProperty('zoneMembers', ListOf(DeviceObjectReference)) + , OptionalProperty('memberOf', ListOf(DeviceObjectReference)) ] @register_object_type @@ -1759,8 +1804,8 @@ class MultiStateInputObject(Object): , OptionalProperty('stateText', ArrayOf(CharacterString)) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1819,8 +1864,8 @@ class MultiStateValueObject(Object): , OptionalProperty('relinquishDefault', Unsigned) , OptionalProperty('timeDelay', Unsigned) , OptionalProperty('notificationClass', Unsigned) - , OptionalProperty('alarmValues', SequenceOf(Unsigned)) - , OptionalProperty('faultValues', SequenceOf(Unsigned)) + , OptionalProperty('alarmValues', ListOf(Unsigned)) + , OptionalProperty('faultValues', ListOf(Unsigned)) , OptionalProperty('eventEnable', EventTransitionBits) , OptionalProperty('ackedTransitions', EventTransitionBits) , OptionalProperty('notifyType', NotifyType) @@ -1847,7 +1892,7 @@ class NetworkSecurityObject(Object): , WritableProperty('lastKeyServer', AddressBinding) , WritableProperty('securityPDUTimeout', Unsigned) , ReadableProperty('updateKeySetTimeout', Unsigned) - , ReadableProperty('supportedSecurityAlgorithms', SequenceOf(Unsigned)) + , ReadableProperty('supportedSecurityAlgorithms', ListOf(Unsigned)) , WritableProperty('doNotHide', Boolean) ] @@ -1858,7 +1903,7 @@ class NotificationClassObject(Object): [ ReadableProperty('notificationClass', Unsigned) , ReadableProperty('priority', ArrayOf(Unsigned)) , ReadableProperty('ackRequired', EventTransitionBits) - , ReadableProperty('recipientList', SequenceOf(Destination)) + , ReadableProperty('recipientList', ListOf(Destination)) ] @register_object_type @@ -1868,8 +1913,8 @@ class NotificationForwarderObject(Object): [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) , ReadableProperty('outOfService', Boolean) - , ReadableProperty('recipientList', SequenceOf(Destination)) - , WritableProperty('subscribedRecipients', SequenceOf(EventNotificationSubscription)) + , ReadableProperty('recipientList', ListOf(Destination)) + , WritableProperty('subscribedRecipients', ListOf(EventNotificationSubscription)) , ReadableProperty('processIdentifierFilter', ProcessIdSelection) , OptionalProperty('portFilter', ArrayOf(PortPermission)) , ReadableProperty('localForwardingOnly', Boolean) @@ -1995,7 +2040,7 @@ class ScheduleObject(Object): , OptionalProperty('weeklySchedule', ArrayOf(DailySchedule)) , OptionalProperty('exceptionSchedule', ArrayOf(SpecialEvent)) , ReadableProperty('scheduleDefault', AnyAtomic) - , ReadableProperty('listOfObjectPropertyReferences', SequenceOf(DeviceObjectPropertyReference)) + , ReadableProperty('listOfObjectPropertyReferences', ListOf(DeviceObjectPropertyReference)) , ReadableProperty('priorityForWriting', Unsigned) , ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('reliability', Reliability) @@ -2064,7 +2109,7 @@ class TrendLogObject(Object): , OptionalProperty('clientCovIncrement', ClientCOV) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogRecord)) + , ReadableProperty('logBuffer', ListOf(LogRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , ReadableProperty('loggingType', LoggingType) @@ -2108,7 +2153,7 @@ class TrendLogMultipleObject(Object): , OptionalProperty('trigger', Boolean) , ReadableProperty('stopWhenFull', Boolean) , ReadableProperty('bufferSize', Unsigned) - , ReadableProperty('logBuffer', SequenceOf(LogMultipleRecord)) + , ReadableProperty('logBuffer', ListOf(LogMultipleRecord)) , WritableProperty('recordCount', Unsigned) , ReadableProperty('totalRecordCount', Unsigned) , OptionalProperty('notificationThreshold', Unsigned) diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index d7d13065..3e89abe1 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -1352,6 +1355,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1380,11 +1386,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1464,19 +1490,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) @@ -1532,6 +1579,7 @@ class ObjectType(Enumerated): , 'accessUser':35 , 'accessZone':36 , 'accumulator':23 + , 'alertEnrollment':52 , 'analogInput':0 , 'analogOutput':1 , 'analogValue':2 @@ -1541,6 +1589,7 @@ class ObjectType(Enumerated): , 'binaryValue':5 , 'bitstringValue':39 , 'calendar':6 + , 'channel':53 , 'characterstringValue':40 , 'command':7 , 'credentialDataInput':37 @@ -1558,6 +1607,7 @@ class ObjectType(Enumerated): , 'largeAnalogValue':46 , 'lifeSafetyPoint':21 , 'lifeSafetyZone':22 + , 'lightingOutput':54 , 'loadControl':28 , 'loop':12 , 'multiStateInput':13 @@ -1565,6 +1615,7 @@ class ObjectType(Enumerated): , 'multiStateValue':19 , 'networkSecurity':38 , 'notificationClass':15 + , 'notificationForwarder':51 , 'octetstringValue':47 , 'positiveIntegerValue':48 , 'program':16 diff --git a/py34/bacpypes/service/cov.py b/py34/bacpypes/service/cov.py index 438e0b22..7e14cb70 100644 --- a/py34/bacpypes/service/cov.py +++ b/py34/bacpypes/service/cov.py @@ -12,7 +12,7 @@ from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ Recipient, RecipientProcess, ObjectPropertyReference -from ..constructeddata import SequenceOf, Any +from ..constructeddata import ListOf, Any from ..apdu import ConfirmedCOVNotificationRequest, \ UnconfirmedCOVNotificationRequest, \ SimpleAckPDU, Error, RejectPDU, AbortPDU @@ -420,7 +420,7 @@ class ActiveCOVSubscriptions(Property): def __init__(self): Property.__init__( - self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + self, 'activeCovSubscriptions', ListOf(COVSubscription), default=None, optional=True, mutable=False, ) @@ -432,7 +432,7 @@ def ReadProperty(self, obj, arrayIndex=None): if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) # start with an empty sequence - cov_subscriptions = SequenceOf(COVSubscription)() + cov_subscriptions = ListOf(COVSubscription)() # loop through the object and detection list for obj, cov_detection in obj._app.cov_detections.items(): diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 7a97051f..11be52b0 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, int): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index ca8d3fe9..3289075d 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/py34/bacpypes/vlan.py b/py34/bacpypes/vlan.py index 953e266e..ead2d03a 100755 --- a/py34/bacpypes/vlan.py +++ b/py34/bacpypes/vlan.py @@ -35,6 +35,9 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.broadcast_address = broadcast_address self.drop_percent = drop_percent + # point to a TrafficLog instance + self.traffic_log = None + def add_node(self, node): """ Add a node to this network, let the node know which network it's on. """ if _debug: Network._debug("add_node %r", node) @@ -59,6 +62,10 @@ def process_pdu(self, pdu): """ if _debug: Network._debug("process_pdu(%s) %r", self.name, pdu) + # if there is a traffic log, call it with the network name and pdu + if self.traffic_log: + self.traffic_log(self.name, pdu) + # randomly drop a packet if self.drop_percent != 0.0: if (random.random() * 100.0) < self.drop_percent: diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index a03026ea..01130394 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -15,7 +15,7 @@ from bacpypes.object import AccumulatorObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.object import ReadWritePropertyMultipleServices # some debugging @@ -84,13 +84,6 @@ def main(): # add the additional service this_application.add_capability(ReadWritePropertyMultipleServices) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a random input object accumulator = AccumulatorObject( objectIdentifier=('accumulator', 1), diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index e416cb00..492a950e 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -18,13 +18,14 @@ from bacpypes.core import run from bacpypes.comm import bind -from bacpypes.pdu import Address +from bacpypes.pdu import Address, LocalBroadcast from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement from bacpypes.bvllservice import BIPBBMD, AnnexJCodec, UDPMultiplexer from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint -from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import WhoIsIAmServices from bacpypes.service.object import ReadWritePropertyServices from bacpypes.primitivedata import Real @@ -209,7 +210,7 @@ def main(): router = VLANRouter(local_address, local_network) # create a VLAN - vlan = Network() + vlan = Network(broadcast_address=LocalBroadcast()) # create a node for the router, address 1 on the VLAN router_node = Node(Address(1)) diff --git a/samples/COVClient.py b/samples/COVClient.py index 29281fa4..e79ba964 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -20,7 +20,7 @@ SimpleAckPDU, RejectPDU, AbortPDU from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -228,13 +228,6 @@ def main(): # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = SubscribeCOVConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/COVServer.py b/samples/COVServer.py index c1c2de8e..ad2558a4 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -18,7 +18,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import AnalogValueObject, BinaryValueObject -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.cov import ChangeOfValueServices # some debugging diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 16b13401..41b8998c 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -26,8 +26,8 @@ TimeValueObject, TimePatternValueObject, ChannelObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.object import CurrentPropertyListMixIn -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.object import CurrentPropertyListMixIn +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index a5247559..e4ce6d63 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -18,7 +18,7 @@ from bacpypes.apdu import DeviceCommunicationControlRequest from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -134,13 +134,6 @@ def main(): # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DCCConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index 7c6b1c82..e0f7276b 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -22,7 +22,7 @@ from bacpypes.errors import DecodingError from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -211,13 +211,6 @@ def main(): # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index fa315b9b..1e7b977e 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -22,7 +22,7 @@ from bacpypes.errors import DecodingError from bacpypes.app import BIPForeignApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -215,13 +215,6 @@ def main(): int(args.ini.foreignttl), ) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 9d99cb4b..61d1c21e 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -22,7 +22,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -190,13 +190,6 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # local host, special port HOST, PORT = "", int(args.port) server = ThreadedTCPServer((HOST, PORT), ThreadedHTTPRequestHandler) diff --git a/samples/HandsOnLab/Sample1_SimpleApplication.py b/samples/HandsOnLab/Sample1_SimpleApplication.py index b96478b7..dd02bd3c 100644 --- a/samples/HandsOnLab/Sample1_SimpleApplication.py +++ b/samples/HandsOnLab/Sample1_SimpleApplication.py @@ -13,7 +13,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -72,13 +72,6 @@ def main(): this_application = SampleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py index 27d00499..2a185f96 100644 --- a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py +++ b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py @@ -15,7 +15,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -90,13 +90,6 @@ def main(): # make a sample application this_application = WhoIsIAmApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py index 9c06c0b2..c44e8dd0 100644 --- a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py +++ b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py @@ -16,7 +16,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -92,13 +92,6 @@ def main(): # make a sample application this_application = WhoHasIHaveApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") # run until stopped, ^C works diff --git a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py index 8aeb5a44..c5de7083 100644 --- a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py +++ b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py @@ -21,7 +21,7 @@ from bacpypes.errors import ExecutionError from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -99,13 +99,6 @@ def main(): # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make some random input objects for i in range(1, RANDOM_OBJECT_COUNT+1): ravo = RandomAnalogValueObject( diff --git a/samples/IP2VLANRouter.py b/samples/IP2VLANRouter.py new file mode 100755 index 00000000..87f36870 --- /dev/null +++ b/samples/IP2VLANRouter.py @@ -0,0 +1,261 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a router sitting on an IP network +to a VLAN. The VLAN has one or more devices on it with an analog +value object that returns a random value for the present value. + +Note that the device instance number of the virtual device will be 100 times +the network number plus its address (net2 * 100 + n). +""" + +import random +import argparse + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind + +from bacpypes.pdu import Address, LocalBroadcast +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property + +from bacpypes.vlan import Network, Node +from bacpypes.errors import ExecutionError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# RandomValueProperty +# + +@bacpypes_debugging +class RandomValueProperty(Property): + + def __init__(self, identifier): + if _debug: RandomValueProperty._debug("__init__ %r", identifier) + Property.__init__(self, identifier, Real, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: RandomValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + + # access an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return a random value + value = random.random() * 100.0 + if _debug: RandomValueProperty._debug(" - value: %r", value) + + return value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# Random Value Object Type +# + +@bacpypes_debugging +class RandomAnalogValueObject(AnalogValueObject): + + properties = [ + RandomValueProperty('presentValue'), + ] + + def __init__(self, **kwargs): + if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) + AnalogValueObject.__init__(self, **kwargs) + +# +# VLANApplication +# + +@bacpypes_debugging +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) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(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() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a vlan node at the assigned address + self.vlan_node = Node(vlan_address) + + # bind the stack to the node, no network number + self.nsap.bind(self.vlan_node) + + def request(self, apdu): + if _debug: VLANApplication._debug("[%s]request %r", self.vlan_node.address, apdu) + Application.request(self, apdu) + + def indication(self, apdu): + if _debug: VLANApplication._debug("[%s]indication %r", self.vlan_node.address, apdu) + Application.indication(self, apdu) + + def response(self, apdu): + if _debug: VLANApplication._debug("[%s]response %r", self.vlan_node.address, apdu) + Application.response(self, apdu) + + def confirmation(self, apdu): + if _debug: VLANApplication._debug("[%s]confirmation %r", self.vlan_node.address, apdu) + Application.confirmation(self, apdu) + +# +# VLANRouter +# + +@bacpypes_debugging +class VLANRouter: + + def __init__(self, local_address, local_network): + if _debug: VLANRouter._debug("__init__ %r %r", local_address, local_network) + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # create a BIPSimple, bound to the Annex J server + # on the UDP multiplexer + self.bip = BIPSimple(local_address) + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(local_address) + + # bind the bottom layers + bind(self.bip, self.annexj, self.mux.annexJ) + + # bind the BIP stack to the local network + self.nsap.bind(self.bip, local_network, local_address) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # add an argument for interval + parser.add_argument('addr1', type=str, + help='address of first network', + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of first network', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of second network', + ) + + # add an argument for how many virtual devices + parser.add_argument('--count', type=int, + help='number of virtual devices', + default=1, + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + local_address = Address(args.addr1) + local_network = args.net1 + vlan_network = args.net2 + + # create the VLAN router, bind it to the local network + router = VLANRouter(local_address, local_network) + + # create a VLAN + vlan = Network(broadcast_address=LocalBroadcast()) + + # create a node for the router, address 1 on the VLAN + router_node = Node(Address(1)) + vlan.add_node(router_node) + + # bind the router stack to the vlan network through this node + router.nsap.bind(router_node, vlan_network) + + # make some devices + for device_number in range(2, 2 + args.count): + # device identifier is assigned from the address + device_instance = vlan_network * 100 + device_number + _log.debug(" - device_instance: %r", device_instance) + + # make a vlan device object + vlan_device = \ + LocalDeviceObject( + objectName="VLAN Node %d" % (device_instance,), + objectIdentifier=('device', device_instance), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - vlan_device: %r", vlan_device) + + # make the application, add it to the network + vlan_app = VLANApplication(vlan_device, Address(device_number)) + vlan.add_node(vlan_app.vlan_node) + _log.debug(" - vlan_app: %r", vlan_app) + + # make a random value object + ravo = RandomAnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='Random1' % (device_instance,), + ) + _log.debug(" - ravo1: %r", ravo) + + # add it to the device + vlan_app.add_object(ravo) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py new file mode 100644 index 00000000..20e6c48b --- /dev/null +++ b/samples/LocalScheduleObject.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python + +""" +This application creates a series of Local Schedule Objects and then prompts +to test dates and times. +""" + +from time import localtime as _localtime + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run + +from bacpypes.primitivedata import Null, Integer, Real, Date, Time, CharacterString +from bacpypes.constructeddata import ArrayOf, SequenceOf +from bacpypes.basetypes import CalendarEntry, DailySchedule, DateRange, \ + DeviceObjectPropertyReference, SpecialEvent, SpecialEventPeriod, TimeValue + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject +from bacpypes.local.schedule import LocalScheduleObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +schedule_objects = [] + +# +# TestConsoleCmd +# + +@bacpypes_debugging +class TestConsoleCmd(ConsoleCmd): + + def do_test(self, args): + """test