diff --git a/BACpypes~.json b/BACpypes~.json new file mode 100644 index 00000000..aa73d190 --- /dev/null +++ b/BACpypes~.json @@ -0,0 +1,17 @@ +{ + "bacpypes": { + "debug": ["__main__"], + "color": true + }, + "local-device": { + "objectName": "Betelgeuse-47808", + "address": "128.253.109.40/24", + "objectIdentifier": 599, + "maxApduLengthAccepted": 1024, + "segmentationSupported": "segmentedBoth", + "maxSegmentsAccepted": 1024, + "vendorIdentifier": 15, + "foreignBBMD": "128.253.109.254", + "foreignTTL": 30 + } +} diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index e4bed942..9e13daa5 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,10 +18,12 @@ # Project Metadata # -__version__ = '0.17.6' +__version__ = '0.17.7' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' +from . import settings + # # Communications Core Modules # diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 4fa8279f..2b7cc91f 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -1581,11 +1581,12 @@ class ReinitializeDeviceRequestReinitializedStateOfDevice(Enumerated): enumerations = \ { 'coldstart':0 , 'warmstart':1 - , 'startbackup':2 - , 'endbackup':3 - , 'startrestore':4 - , 'endrestore':5 - , 'abortrestore':6 + , 'startBackup':2 + , 'endBackup':3 + , 'startRestore':4 + , 'endRestore':5 + , 'abortRestore':6 + , 'activateChanges':7 } class ReinitializeDeviceRequest(ConfirmedRequestSequence): diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index 89691b9b..110dee3e 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -516,7 +516,6 @@ def segmented_request(self, apdu): if _debug: ClientSSM._debug(" - error/reject/abort") self.set_state(COMPLETED) - self.response = apdu self.response(apdu) else: diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index 6e5b75e5..4d479a28 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -4,11 +4,12 @@ Base Types """ -from .debugging import ModuleLogger +from .debugging import bacpypes_debugging, ModuleLogger +from .errors import MissingRequiredParameter -from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ +from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned, Unsigned16 + Unsigned, Unsigned16, Tag from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -1719,9 +1720,87 @@ class RouterEntry(Sequence): class NameValue(Sequence): sequenceElements = \ [ Element('name', CharacterString) - , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + , Element('value', AnyAtomic, None, True) ] + def __init__(self, name=None, value=None): + if _debug: NameValue._debug("__init__ name=%r value=%r", name, value) + + # default to no value + self.name = name + self.value = None + + if value is None: + pass + elif isinstance(value, (Atomic, DateTime)): + self.value = value + elif isinstance(value, Tag): + self.value = value.app_to_object() + else: + raise TypeError("invalid constructor datatype") + + def encode(self, taglist): + if _debug: NameValue._debug("(%r)encode %r", self.__class__.__name__, taglist) + + # build a tag and encode the name into it + tag = Tag() + CharacterString(self.name).encode(tag) + taglist.append(tag.app_to_context(0)) + + # the value is optional + if self.value is not None: + if isinstance(self.value, DateTime): + # has its own encoder + self.value.encode(taglist) + else: + # atomic values encode into a tag + tag = Tag() + self.value.encode(tag) + taglist.append(tag) + + def decode(self, taglist): + if _debug: NameValue._debug("(%r)decode %r", self.__class__.__name__, taglist) + + # no contents yet + self.name = None + self.value = None + + # look for the context encoded character string + tag = taglist.Peek() + if _debug: NameValue._debug(" - name tag: %r", tag) + if (tag is None) or (tag.tagClass != Tag.contextTagClass) or (tag.tagNumber != 0): + raise MissingRequiredParameter("%s is a missing required element of %s" % ('name', self.__class__.__name__)) + + # pop it off and save the value + taglist.Pop() + tag = tag.context_to_app(Tag.characterStringAppTag) + self.name = CharacterString(tag).value + + # look for the optional application encoded value + tag = taglist.Peek() + if _debug: NameValue._debug(" - value tag: %r", tag) + if tag and (tag.tagClass == Tag.applicationTagClass): + + # if it is a date check the next one for a time + if (tag.tagNumber == Tag.dateAppTag) and (len(taglist.tagList) >= 2): + next_tag = taglist.tagList[1] + if _debug: NameValue._debug(" - next_tag: %r", next_tag) + + if (next_tag.tagClass == Tag.applicationTagClass) and (next_tag.tagNumber == Tag.timeAppTag): + if _debug: NameValue._debug(" - remaining tag list 0: %r", taglist.tagList) + + self.value = DateTime() + self.value.decode(taglist) + if _debug: NameValue._debug(" - date time value: %r", self.value) + + # just a primitive value + if self.value is None: + taglist.Pop() + self.value = tag.app_to_object() + +bacpypes_debugging(NameValue) + +>>>>>>> stage class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py index bab2bdd3..7c177d91 100644 --- a/py25/bacpypes/local/device.py +++ b/py25/bacpypes/local/device.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from ..debugging import bacpypes_debugging, ModuleLogger +from ..debugging import bacpypes_debugging, ModuleLogger, xtob from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ OctetString, CharacterString, BitString, Enumerated, Date, Time, \ diff --git a/py25/bacpypes/local/file.py b/py25/bacpypes/local/file.py index 3792526a..d3595bd6 100644 --- a/py25/bacpypes/local/file.py +++ b/py25/bacpypes/local/file.py @@ -1,16 +1,9 @@ #!/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()) diff --git a/py25/bacpypes/local/object.py b/py25/bacpypes/local/object.py index 5b5be5d3..a4d55c37 100644 --- a/py25/bacpypes/local/object.py +++ b/py25/bacpypes/local/object.py @@ -1,12 +1,26 @@ #!/usr/bin/env python +import re + from ..debugging import bacpypes_debugging, ModuleLogger -from ..basetypes import PropertyIdentifier -from ..constructeddata import ArrayOf +from ..task import OneShotTask +from ..primitivedata import Atomic, Null, BitString, CharacterString, \ + Date, Integer, Double, Enumerated, OctetString, Real, Time, Unsigned +from ..basetypes import PropertyIdentifier, DateTime, NameValue, BinaryPV, \ + ChannelValue, DoorValue, PriorityValue, PriorityArray +from ..constructeddata import Array, ArrayOf, SequenceOf from ..errors import ExecutionError -from ..object import Property, Object +from ..object import Property, ReadableProperty, WritableProperty, OptionalProperty, Object, \ + AccessDoorObject, AnalogOutputObject, AnalogValueObject, \ + BinaryOutputObject, BinaryValueObject, BitStringValueObject, CharacterStringValueObject, \ + DateValueObject, DatePatternValueObject, DateTimePatternValueObject, \ + DateTimeValueObject, IntegerValueObject, \ + LargeAnalogValueObject, LightingOutputObject, MultiStateOutputObject, \ + MultiStateValueObject, OctetStringValueObject, PositiveIntegerValueObject, \ + TimeValueObject, TimePatternValueObject, ChannelObject + # some debugging _debug = 0 @@ -67,3 +81,810 @@ class CurrentPropertyListMixIn(Object): CurrentPropertyList(), ] +# +# Turtle Reference Patterns +# + +# character reference patterns +HEX = u"[0-9A-Fa-f]" +PERCENT = u"%" + HEX + HEX +UCHAR = u"[\\\]u" + HEX * 4 + "|" + u"[\\\]U" + HEX * 8 + +# character sets +PN_CHARS_BASE = ( + u"A-Za-z" + u"\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF" + u"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + u"\uFDF0-\uFFFD\U00010000-\U000EFFFF" +) + +PN_CHARS_U = PN_CHARS_BASE + u"_" +PN_CHARS = u"-" + PN_CHARS_U + u"0-9\u00B7\u0300-\u036F\u203F-\u2040" + +# patterns +IRIREF = u'[<]([^\u0000-\u0020<>"{}|^`\\\]|' + UCHAR + u")*[>]" +PN_PREFIX = u"[" + PN_CHARS_BASE + u"](([." + PN_CHARS + u"])*[" + PN_CHARS + u"])?" + +PN_LOCAL_ESC = u"[-\\_~.!$&'()*+,;=/?#@%]" +PLX = u"(" + PERCENT + u"|" + PN_LOCAL_ESC + u")" + +# non-prefixed names +PN_LOCAL = ( + u"([" + + PN_CHARS_U + + u":0-9]|" + + PLX + + u")(([" + + PN_CHARS + + u".:]|" + + PLX + + u")*([" + + PN_CHARS + + u":]|" + + PLX + + u"))?" +) + +# namespace prefix declaration +PNAME_NS = u"(" + PN_PREFIX + u")?:" + +# prefixed names +PNAME_LN = PNAME_NS + PN_LOCAL + +# blank nodes +BLANK_NODE_LABEL = ( + u"_:[" + PN_CHARS_U + u"0-9]([" + PN_CHARS + u".]*[" + PN_CHARS + u"])?" +) + +# see https://www.w3.org/TR/turtle/#sec-parsing-terms +iriref_re = re.compile(u"^" + IRIREF + u"$", re.UNICODE) +local_name_re = re.compile(u"^" + PN_LOCAL + u"$", re.UNICODE) +namespace_prefix_re = re.compile(u"^" + PNAME_NS + u"$", re.UNICODE) +prefixed_name_re = re.compile(u"^" + PNAME_LN + u"$", re.UNICODE) +blank_node_re = re.compile(u"^" + BLANK_NODE_LABEL + u"$", re.UNICODE) + +# see https://tools.ietf.org/html/bcp47#section-2.1 for better syntax +language_tag_re = re.compile(u"^[A-Za-z0-9-]+$", re.UNICODE) + +class IRI: + # regex from RFC 3986 + _e = r"^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?" + _p = re.compile(_e) + _default_ports = (("http", ":80"), ("https", ":443")) + + def __init__(self, iri=None): + self.iri = iri + + if not iri: + g = (None, None, None, None, None) + else: + m = IRI._p.match(iri) + if not m: + raise ValueError("not an IRI") + + # remove default http and https ports + g = list(m.groups()) + for scheme, suffix in IRI._default_ports: + if (g[0] == scheme) and g[1] and g[1].endswith(suffix): + g[1] = g[1][: g[1].rfind(":")] + break + + self.scheme, self.authority, self.path, self.query, self.fragment = g + + def __str__(self): + rval = "" + if self.scheme: + rval += self.scheme + ":" + if self.authority is not None: + rval += "//" + self.authority + if self.path is not None: + rval += self.path + if self.query is not None: + rval += "?" + self.query + if self.fragment is not None: + rval += "#" + self.fragment + return rval + + def is_local_name(self): + if not all( + ( + self.scheme is None, + self.authority is None, + self.path, + self.query is None, + self.fragment is None, + ) + ): + return False + if self.path.startswith(":") or "/" in self.path: # term is not ':x' + return False + return True + + def is_prefix(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path == "" # term is 'x:' + else: + return self.path == ":" # term is ':' + + def is_prefixed_name(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path != "" # term is 'x:y' + else: # term is ':y' but not ':' + return self.path and (self.path != ":") and self.path.startswith(":") + + def resolve(self, iri): + """Resolve a relative IRI to this IRI as a base.""" + # parse the IRI if necessary + if isinstance(iri, str): + iri = IRI(iri) + elif not isinstance(iri, IRI): + raise TypeError("iri must be an IRI or a string") + + # return an IRI object + rslt = IRI() + + if iri.scheme and iri.scheme != self.scheme: + rslt.scheme = iri.scheme + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.scheme = self.scheme + + if iri.authority is not None: + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.authority = self.authority + + if not iri.path: + rslt.path = self.path + if iri.query is not None: + rslt.query = iri.query + else: + rslt.query = self.query + else: + if iri.path.startswith("/"): + # IRI represents an absolute path + rslt.path = iri.path + else: + # merge paths + path = self.path + + # append relative path to the end of the last + # directory from base + path = path[0 : path.rfind("/") + 1] + if len(path) > 0 and not path.endswith("/"): + path += "/" + path += iri.path + + rslt.path = path + + rslt.query = iri.query + + # normalize path + if rslt.path != "": + rslt.remove_dot_segments() + + rslt.fragment = iri.fragment + + return rslt + + def remove_dot_segments(self): + # empty path shortcut + if len(self.path) == 0: + return + + input_ = self.path.split("/") + output_ = [] + + while len(input_) > 0: + next = input_.pop(0) + done = len(input_) == 0 + + if next == ".": + if done: + # ensure output has trailing / + output_.append("") + continue + + if next == "..": + if len(output_) > 0: + output_.pop() + if done: + # ensure output has trailing / + output_.append("") + continue + + output_.append(next) + + # ensure output has leading / + if len(output_) > 0 and output_[0] != "": + output_.insert(0, "") + if len(output_) == 1 and output_[0] == "": + return "/" + + self.path = "/".join(output_) + +bacpypes_debugging(CurrentPropertyList) + + +class TagSet: + def index(self, name, value=None): + """Find the first name with dictionary semantics or (name, value) with + list semantics.""" + if _debug: TagSet._debug("index %r %r", name, value) + + # if this is a NameValue rip it apart first + if isinstance(name, NameValue): + name, value = name.name, name.value + + # no value then look for first matching name + if value is None: + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if name == v.name: + return i + else: + raise KeyError(name) + + # skip int values, it is the zeroth element of an array but does + # not exist for a list + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if ( + name == v.name + and isinstance(value, type(v.value)) + and value.value == v.value.value + ): + return i + else: + raise ValueError((name, value)) + + def add(self, name, value=None): + """Add a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("add %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + # name is a string + if not isinstance(name, str): + raise TypeError("name must be a string, got %r" % (type(name),)) + + # reserved directive names + if name.startswith("@"): + if name == "@base": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@base') + if v and v.value == value.value: + pass + else: + raise ValueError("@base exists") + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + elif name == "@id": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@id') + if v and v.value == value.value: + pass + else: + raise ValueError("@id exists") + +# # check the patterns +# for pattern in (blank_node_re, prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(value.value): +# break +# else: +# raise ValueError("invalid value for @id") + + elif name == "@language": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get("@language") + if v and v.value == value.value: + pass + else: + raise ValueError("@language exists") + + if not language_tag_re.match(value.value): + raise ValueError("value must be a language tag") + + elif name == "@vocab": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@vocab') + if v and v.value == value.value: + pass + else: + raise ValueError("@vocab exists") + + else: + raise ValueError("invalid directive name") + + elif name.endswith(":"): + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get(name) + if v and v.value == value.value: + pass + else: + raise ValueError("prefix exists: %r" % (name,)) + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + else: +# # check the patterns +# for pattern in (prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(name): +# break +# else: +# raise ValueError("invalid name") + pass + + # check the value + if not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # see if the (name, value) already exists + try: + self.index(name, value) + except ValueError: + super(TagSet, self).append(NameValue(name=name, value=value)) + + def discard(self, name, value=None): + """Discard a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("discard %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + indx = self.index(name, value) + return super(TagSet, self).__delitem__(indx) + + def append(self, name_value): + """Override the append operation for mutable set semantics.""" + if _debug: TagSet._debug("append %r", name_value) + + if not isinstance(name_value, NameValue): + raise TypeError + + # turn this into an add operation + self.add(name_value.name, name_value.value) + + def get(self, key, default=None): + """Get the value of a key or default value if the key was not found, + dictionary semantics.""" + if _debug: TagSet._debug("get %r %r", key, default) + + try: + if not isinstance(key, str): + raise TypeError(key) + return self.value[self.index(key)].value + except KeyError: + return default + + def __getitem__(self, item): + """If item is an integer, return the value of the NameValue element + with array/sequence semantics. If the item is a string, return the + value with dictionary semantics.""" + if _debug: TagSet._debug("__getitem__ %r", item) + + # integers imply index + if isinstance(item, int): + return super(TagSet, self).__getitem__(item) + + return self.value[self.index(item)] + + def __setitem__(self, item, value): + """If item is an integer, change the value of the NameValue element + with array/sequence semantics. If the item is a string, change the + current value or add a new value with dictionary semantics.""" + if _debug: TagSet._debug("__setitem__ %r %r", item, value) + + # integers imply index + if isinstance(item, int): + indx = item + if indx < 0: + raise IndexError("assignment index out of range") + elif isinstance(self, Array): + if indx == 0 or indx > len(self.value): + raise IndexError + elif indx >= len(self.value): + raise IndexError + elif isinstance(item, str): + try: + indx = self.index(item) + except KeyError: + self.add(item, value) + return + else: + raise TypeError(repr(item)) + + # check the value + if value is None: + value = Null() + elif not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # now we're good to go + self.value[indx].value = value + + def __delitem__(self, item): + """If the item is a integer, delete the element with array semantics, or + if the item is a string, delete the element with dictionary semantics, + or (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("__delitem__ %r", item) + + # integers imply index + if isinstance(item, int): + indx = item + elif isinstance(item, str): + indx = self.index(item) + elif isinstance(item, tuple): + indx = self.index(*item) + else: + raise TypeError(item) + + return super(TagSet, self).__delitem__(indx) + + def __contains__(self, key): + if _debug: TagSet._debug("__contains__ %r", key) + + try: + if isinstance(key, tuple): + self.index(*key) + elif isinstance(key, str): + self.index(key) + else: + raise TypeError(key) + + return True + except (KeyError, ValueError): + return False + +bacpypes_debugging(TagSet) + +class ArrayOfNameValue(TagSet, ArrayOf(NameValue)): + pass + + +class SequenceOfNameValue(TagSet, SequenceOf(NameValue)): + pass + + +class TagsMixIn(Object): + properties = \ + [ OptionalProperty('tags', ArrayOfNameValue) + ] + + +def Commandable(datatype, presentValue='presentValue', priorityArray='priorityArray', relinquishDefault='relinquishDefault'): + if _debug: Commandable._debug("Commandable %r ...", datatype) + + class _Commando(object): + + properties = [ + WritableProperty(presentValue, datatype), + ReadableProperty(priorityArray, PriorityArray), + ReadableProperty(relinquishDefault, datatype), + ] + + _pv_choice = None + + def __init__(self, **kwargs): + super(_Commando, self).__init__(**kwargs) + + # build a default value in case one is needed + default_value = datatype().value + if issubclass(datatype, Enumerated): + default_value = datatype._xlate_table[default_value] + if _debug: Commandable._debug(" - default_value: %r", default_value) + + # see if a present value was provided + if (presentValue not in kwargs): + setattr(self, presentValue, default_value) + + # see if a priority array was provided + if (priorityArray not in kwargs): + setattr(self, priorityArray, PriorityArray()) + + # see if a present value was provided + if (relinquishDefault not in kwargs): + setattr(self, relinquishDefault, default_value) + + def _highest_priority_value(self): + if _debug: Commandable._debug("_highest_priority_value") + + priority_array = getattr(self, priorityArray) + for i in range(1, 17): + priority_value = priority_array[i] + if priority_value.null is None: + if _debug: Commandable._debug(" - found at index: %r", i) + + value = getattr(priority_value, _Commando._pv_choice) + value_source = "###" + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + break + else: + value = getattr(self, relinquishDefault) + value_source = None + + if _debug: Commandable._debug(" - value, value_source: %r, %r", value, value_source) + + # return what you found + return value, value_source + + def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct=False): + if _debug: Commandable._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", property, value, arrayIndex, priority, direct) + + # when writing to the presentValue with a priority + if (property == presentValue): + if _debug: Commandable._debug(" - writing to %s, priority %r", presentValue, priority) + + # default (lowest) priority + if priority is None: + priority = 16 + if _debug: Commandable._debug(" - translate to priority array, index %d", priority) + + # translate to updating the priority array + property = priorityArray + arrayIndex = priority + priority = None + + # update the priority array entry + if (property == priorityArray): + if (arrayIndex is None): + if _debug: Commandable._debug(" - writing entire %s", priorityArray) + + # pass along the request + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + else: + if _debug: Commandable._debug(" - writing to %s, array index %d", priorityArray, arrayIndex) + + # check the bounds + if arrayIndex == 0: + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + if (arrayIndex < 1) or (arrayIndex > 16): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + + # update the specific priorty value element + priority_value = getattr(self, priorityArray)[arrayIndex] + if _debug: Commandable._debug(" - priority_value: %r", priority_value) + + # the null or the choice has to be set, the other clear + if value is (): + if _debug: Commandable._debug(" - write a null") + priority_value.null = value + setattr(priority_value, _Commando._pv_choice, None) + else: + if _debug: Commandable._debug(" - write a value") + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + priority_value.null = None + setattr(priority_value, _Commando._pv_choice, value) + + # look for the highest priority value + value, value_source = self._highest_priority_value() + + # compare with the current value + current_value = getattr(self, presentValue) + if value == current_value: + if _debug: Commandable._debug(" - no present value change") + return + + # turn this into a present value change + property = presentValue + arrayIndex = priority = None + + # allow the request to pass through + if _debug: Commandable._debug(" - super: %r %r arrayIndex=%r priority=%r", property, value, arrayIndex, priority) + + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + + # look up a matching priority value choice + for element in PriorityValue.choiceElements: + if issubclass(datatype, element.klass): + _Commando._pv_choice = element.name + break + else: + _Commando._pv_choice = 'constructedValue' + if _debug: Commandable._debug(" - _pv_choice: %r", _Commando._pv_choice) + + # return the class + return _Commando + +bacpypes_debugging(Commandable) + +# +# MinOnOffTask +# + +class MinOnOffTask(OneShotTask): + + def __init__(self, binary_obj): + if _debug: MinOnOffTask._debug("__init__ %s", repr(binary_obj)) + OneShotTask.__init__(self) + + # save a reference to the object + self.binary_obj = binary_obj + + # listen for changes to the present value + self.binary_obj._property_monitors['presentValue'].append(self.present_value_change) + + def present_value_change(self, old_value, new_value): + if _debug: MinOnOffTask._debug("present_value_change %r %r", old_value, new_value) + + # if there's no value change, skip all this + if old_value == new_value: + if _debug: MinOnOffTask._debug(" - no state change") + return + + # get the minimum on/off time + if new_value == 'inactive': + task_delay = getattr(self.binary_obj, 'minimumOnTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum on: %r", task_delay) + elif new_value == 'active': + task_delay = getattr(self.binary_obj, 'minimumOffTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum off: %r", task_delay) + else: + raise ValueError("unrecognized present value for %r: %r" % (self.binary_obj.objectIdentifier, new_value)) + + # if there's no delay, don't bother + if not task_delay: + if _debug: MinOnOffTask._debug(" - no delay") + return + + # set the value at priority 6 + self.binary_obj.WriteProperty('presentValue', new_value, priority=6) + + # install this to run, if there is a delay + self.install_task(delta=task_delay) + + def process_task(self): + if _debug: MinOnOffTask._debug("process_task(%s)", self.binary_obj.objectName) + + # clear the value at priority 6 + self.binary_obj.WriteProperty('presentValue', (), priority=6) + +bacpypes_debugging(MinOnOffTask) + +# +# MinOnOff +# + +class MinOnOff(object): + + def __init__(self, **kwargs): + if _debug: MinOnOff._debug("__init__ ...") + super(MinOnOff, self).__init__(**kwargs) + + # create the timer task + self._min_on_off_task = MinOnOffTask(self) + +bacpypes_debugging(MinOnOff) + +# +# Commandable Standard Objects +# + +class AccessDoorCmdObject(Commandable(DoorValue), AccessDoorObject): + pass + +class AnalogOutputCmdObject(Commandable(Real), AnalogOutputObject): + pass + +class AnalogValueCmdObject(Commandable(Real), AnalogValueObject): + pass + +### class BinaryLightingOutputCmdObject(Commandable(Real), BinaryLightingOutputObject): +### pass + +class BinaryOutputCmdObject(Commandable(BinaryPV), MinOnOff, BinaryOutputObject): + pass + +class BinaryValueCmdObject(Commandable(BinaryPV), MinOnOff, BinaryValueObject): + pass + +class BitStringValueCmdObject(Commandable(BitString), BitStringValueObject): + pass + +class CharacterStringValueCmdObject(Commandable(CharacterString), CharacterStringValueObject): + pass + +class DateValueCmdObject(Commandable(Date), DateValueObject): + pass + +class DatePatternValueCmdObject(Commandable(Date), DatePatternValueObject): + pass + +class DateTimeValueCmdObject(Commandable(DateTime), DateTimeValueObject): + pass + +class DateTimePatternValueCmdObject(Commandable(DateTime), DateTimePatternValueObject): + pass + +class IntegerValueCmdObject(Commandable(Integer), IntegerValueObject): + pass + +class LargeAnalogValueCmdObject(Commandable(Double), LargeAnalogValueObject): + pass + +class LightingOutputCmdObject(Commandable(Real), LightingOutputObject): + pass + +class MultiStateOutputCmdObject(Commandable(Unsigned), MultiStateOutputObject): + pass + +class MultiStateValueCmdObject(Commandable(Unsigned), MultiStateValueObject): + pass + +class OctetStringValueCmdObject(Commandable(OctetString), OctetStringValueObject): + pass + +class PositiveIntegerValueCmdObject(Commandable(Unsigned), PositiveIntegerValueObject): + pass + +class TimeValueCmdObject(Commandable(Time), TimeValueObject): + pass + +class TimePatternValueCmdObject(Commandable(Time), TimePatternValueObject): + pass + +class ChannelValueProperty(Property): + + def __init__(self): + if _debug: ChannelValueProperty._debug("__init__") + Property.__init__(self, 'presentValue', ChannelValue, default=None, optional=False, mutable=True) + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: ChannelValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + + ### Clause 12.53.5, page 487 + raise NotImplementedError() + +bacpypes_debugging(ChannelValueProperty) + +class ChannelCmdObject(ChannelObject): + + properties = [ + ChannelValueProperty(), + ] diff --git a/py25/bacpypes/local/schedule.py b/py25/bacpypes/local/schedule.py index a6f368be..e9541bc3 100644 --- a/py25/bacpypes/local/schedule.py +++ b/py25/bacpypes/local/schedule.py @@ -4,7 +4,6 @@ Local Schedule Object """ -import sys import calendar from time import mktime as _mktime diff --git a/py25/bacpypes/netservice.py b/py25/bacpypes/netservice.py index 35aff03d..71cf2a1b 100755 --- a/py25/bacpypes/netservice.py +++ b/py25/bacpypes/netservice.py @@ -9,6 +9,7 @@ from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError +from .core import deferred from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement from .task import FunctionTask @@ -26,7 +27,7 @@ ROUTER_AVAILABLE = 0 # normal ROUTER_BUSY = 1 # router is busy ROUTER_DISCONNECTED = 2 # could make a connection, but hasn't -ROUTER_UNREACHABLE = 3 # cannot route +ROUTER_UNREACHABLE = 3 # temporarily unreachable # # RouterInfo @@ -36,13 +37,17 @@ class RouterInfo(DebugContents): """These objects are routing information records that map router addresses with destination networks.""" - _debug_contents = ('snet', 'address', 'dnets', 'status') + _debug_contents = ('snet', 'address', 'dnets') - def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + def __init__(self, snet, address): 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 + self.dnets = {} # {dnet: status} + + def set_status(self, dnets, status): + """Change the status of each of the DNETS.""" + for dnet in dnets: + self.dnets[dnet] = status # # RouterInfoCache @@ -53,111 +58,123 @@ 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) + self.routers = {} # snet -> {Address: RouterInfo} + self.path_info = {} # (snet, dnet) -> RouterInfo - # check to see if we know about it - if dnet not in self.networks: - if _debug: RouterInfoCache._debug(" - no route") - return None + def get_router_info(self, snet, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r %r", snet, dnet) # return the network and address - router_info = self.networks[dnet] + router_info = self.path_info.get((snet, dnet), None) 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) + return router_info - def update_router_info(self, snet, address, dnets): + def update_router_info(self, snet, address, dnets, status=ROUTER_AVAILABLE): 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] + existing_router_info = self.routers.get(snet, {}).get(address, None) - # add (or move) the destination networks + other_routers = set() 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") + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + if other_routers: + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + # update current router info if there is one + if not existing_router_info: + router_info = RouterInfo(snet, address) + if snet not in self.routers: + self.routers[snet] = {address: router_info} + else: + self.routers[snet][address] = router_info - # 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) + for dnet in dnets: + self.path_info[(snet, dnet)] = router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r via %r", snet, dnet, router_info.address) + router_info.dnets[dnet] = status + else: + for dnet in dnets: + if dnet not in existing_router_info.dnets: + self.path_info[(snet, dnet)] = existing_router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r via %r", snet, dnet, router_info.address) + existing_router_info.dnets[dnet] = status 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") + existing_router_info = self.routers.get(snet, {}).get(address, None) + if not existing_router_info: + if _debug: RouterInfoCache._debug(" - not a router we know about") return - router_info = self.routers[key] - router_info.status = status - if _debug: RouterInfoCache._debug(" - status updated") + existing_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 + if (address is None) and (dnets is None): + raise RuntimeError("inconsistent parameters") - # look up the router reference - key = (snet, address) - if key not in self.routers: - if _debug: RouterInfoCache._debug(" - unknown router") + # remove the dnets from a router or the whole router + if (address is not None): + router_info = self.routers.get(snet, {}).get(address, None) + if not router_info: + if _debug: RouterInfoCache._debug(" - no route info") + else: + for dnet in (dnets or router_info.dnets): + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + del self.routers[snet][address] 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 + # look for routers to the dnets + other_routers = set() 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) + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + def update_source_network(self, old_snet, new_snet): + if _debug: RouterInfoCache._debug("update_source_network %r %r", old_snet, new_snet) + + if old_snet not in self.routers: + if _debug: RouterInfoCache._debug(" - no router references: %r", list(self.routers.keys())) + return + + # move the router info records to the new net + snet_routers = self.routers[new_snet] = self.routers.pop(old_snet) - # see if we still care - if not router_info.dnets: - if _debug: RouterInfoCache._debug(" - no longer care about this router") - del self.routers[key] + # update the paths + for address, router_info in snet_routers.items(): + for dnet in router_info.dnets: + self.path_info[(new_snet, dnet)] = self.path_info.pop((old_snet, dnet)) -bacpypes_debugging(RouterInfoCache) +bacpypes_debugging(RotuerInfoCache) # # NetworkAdapter @@ -165,13 +182,19 @@ def delete_router_info(self, snet, address=None, dnets=None): class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') + _debug_contents = ( + 'adapterSAP-', + 'adapterNet', + 'adapterAddr', + 'adapterNetConfigured', + ) - def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) + def __init__(self, sap, net, addr, cid=None): + if _debug: NetworkAdapter._debug("__init__ %s %r %r cid=%r", sap, net, addr, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net + self.adapterAddr = addr # record if this was 0=learned, 1=configured, None=unknown if net is None: @@ -210,7 +233,7 @@ def DisconnectConnectionToNetwork(self, net): class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): _debug_contents = ('adapters++', 'pending_nets', - 'local_adapter-', 'local_address', + 'local_adapter-', ) def __init__(self, router_info_cache=None, sap=None, sid=None): @@ -227,36 +250,60 @@ def __init__(self, router_info_cache=None, sap=None, sid=None): # map to a list of application layer packets waiting for a path self.pending_nets = {} - # these are set when bind() is called + # 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.""" + """Create a network adapter object and bind. + + bind(s, None, None) + Called for simple applications, local network unknown, no specific + address, APDUs sent upstream + + bind(s, net, None) + Called for routers, bind to the network, (optionally?) drop APDUs + + bind(s, None, address) + Called for applications or routers, bind to the network (to be + discovered), send up APDUs with a metching address + + bind(s, net, address) + Called for applications or routers, bind to the network, send up + APDUs with a metching address. + """ if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) # make sure this hasn't already been called with this network if net in self.adapters: - raise RuntimeError("already bound") + raise RuntimeError("already bound: %r" % (net,)) # create an adapter object, add it to our map - adapter = NetworkAdapter(self, net) + adapter = NetworkAdapter(self, net, address) self.adapters[net] = adapter - if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r, %r", net, adapter) # if the address was given, make it the "local" one - if address and not self.local_address: + if address: + if _debug: NetworkServiceAccessPoint._debug(" - setting local adapter") + self.local_adapter = adapter + + # if the local adapter isn't set yet, make it the first one, and can + # be overridden by a subsequent call if the address is specified + if not self.local_adapter: + if _debug: NetworkServiceAccessPoint._debug(" - default local adapter") self.local_adapter = adapter - self.local_address = address + + if not self.local_adapter.adapterAddr: + if _debug: NetworkServiceAccessPoint._debug(" - no local address") # bind to the server bind(adapter, server) #----- - def add_router_references(self, snet, address, dnets): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) + def update_router_references(self, snet, address, dnets): + """Update references to routers.""" + if _debug: NetworkServiceAccessPoint._debug("update_router_references %r %r %r", snet, address, dnets) # see if we have an adapter for the snet if snet not in self.adapters: @@ -285,13 +332,9 @@ def indication(self, pdu): if (not self.adapters): raise ConfigurationError("no adapters") - # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.local_adapter): - raise ConfigurationError("local adapter must be set") - # get the local adapter - adapter = self.local_adapter or self.adapters[None] - if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) + local_adapter = self.local_adapter + if _debug: NetworkServiceAccessPoint._debug(" - local_adapter: %r", local_adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -308,12 +351,12 @@ def indication(self, pdu): # local stations given to local adapter if (npdu.pduDestination.addrType == Address.localStationAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # local broadcast given to local adapter if (npdu.pduDestination.addrType == Address.localBroadcastAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # global broadcast @@ -332,9 +375,10 @@ def indication(self, pdu): raise RuntimeError("invalid destination address type: %s" % (npdu.pduDestination.addrType,)) dnet = npdu.pduDestination.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - dnet: %r", dnet) # if the network matches the local adapter it's local - if (dnet == adapter.adapterNet): + if (dnet == local_adapter.adapterNet): if (npdu.pduDestination.addrType == Address.remoteStationAddr): if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) @@ -344,7 +388,7 @@ def indication(self, pdu): else: raise RuntimeError("addressing problem") - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # get it ready to send when the path is found @@ -357,49 +401,50 @@ def indication(self, pdu): self.pending_nets[dnet].append(npdu) return - # check cache for an available path - path_info = self.router_info_cache.get_router_info(dnet) + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break # 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) + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - router_info found: %r", router_info) - # 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) + ### check the path status + dnet_status = router_info.dnets[dnet] + if _debug: NetworkServiceAccessPoint._debug(" - dnet_status: %r", dnet_status) # fix the destination - npdu.pduDestination = address + npdu.pduDestination = router_info.address # send it along - adapter.process_npdu(npdu) - return + snet_adapter.process_npdu(npdu) - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") + else: + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # 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) + # 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() + # 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 adapter in self.adapters.values(): - ### make sure the adapter is OK - self.sap_indication(adapter, xnpdu) + # send it to all of the adapters + for adapter in self.adapters.values(): + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) # make sure our configuration is OK - if (not self.adapters): + if not self.adapters: raise ConfigurationError("no adapters") # check for source routing @@ -412,29 +457,14 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (1)") return - # 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) - - # see if the router has changed - if not (router_address == npdu.pduSource): - if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) - else: - if _debug: NetworkServiceAccessPoint._debug(" - new path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) + # 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): if _debug: NetworkServiceAccessPoint._debug(" - no DADR") - processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) + processLocally = (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -444,8 +474,7 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: @@ -455,9 +484,8 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_adapter.adapterAddr.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: @@ -496,13 +524,13 @@ def process_npdu(self, adapter, npdu): # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: apdu.pduDestination = npdu.npduDADR elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr else: # combine the source address if npdu.npduSADR: @@ -606,33 +634,27 @@ def process_npdu(self, adapter, npdu): xadapter.process_npdu(_deepcopy(newpdu)) return - # 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 + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break - xadapter = self.adapters[router_net] - if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) + # found a path + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", router_info) # the destination is the address of the router - newpdu.pduDestination = router_address + newpdu.pduDestination = router_info.address # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) + snet_adapter.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 xnpdu = WhoIsRouterToNetwork(dnet) xnpdu.pduDestination = LocalBroadcast() @@ -680,6 +702,8 @@ def sap_confirmation(self, adapter, npdu): class NetworkServiceElement(ApplicationServiceElement): + _startup_disabled = False + def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) @@ -687,6 +711,49 @@ def __init__(self, eid=None): # network number is timeout self.network_number_is_task = None + # if starting up is enabled defer our startup function + if not self._startup_disabled: + deferred(self.startup) + + def startup(self): + if _debug: NetworkServiceElement._debug("startup") + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # loop through all of the adapters + for adapter in sap.adapters.values(): + if _debug: NetworkServiceElement._debug(" - adapter: %r", adapter) + + if (adapter.adapterNet is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown net") + continue + elif (adapter.adapterAddr is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown addr") + continue + + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + if (xadapter.adapterNet is None) or (xadapter.adapterAddr is None): + continue + netlist.append(xadapter.adapterNet) + + # skip for an empty list, perhaps they are not yet learned + if not netlist: + if _debug: NetworkServiceElement._debug(" - skipping, no netlist") + continue + + # pass this along to the cache -- on hold #213 + # sap.router_info_cache.update_router_info(adapter.adapterNet, adapter.adapterAddr, netlist) + + # send an announcement + self.i_am_router_to_network(adapter=adapter, network=netlist) + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -767,10 +834,14 @@ def i_am_router_to_network(self, adapter=None, destination=None, network=None): netlist.append(xadapter.adapterNet) ### add the other reachable networks - if network is not None: + if network is None: + pass + elif isinstance(network, int): if network not in netlist: continue netlist = [network] + elif isinstance(network, list): + netlist = [net for net in netlist if net in network] # build a response iamrtn = IAmRouterToNetwork(netlist) @@ -810,7 +881,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # add the direct network netlist.append(xadapter.adapterNet) - ### add the other reachable + ### add the other reachable networks? if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -841,51 +912,51 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # send it back self.response(adapter, iamrtn) + return - else: - # see if there is routing information for this source network - router_info = sap.router_info_cache.get_router_info(dnet) + + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in sap.adapters.items(): + router_info = sap.router_info_cache.get_router_info(snet, 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 - if sap.adapters[router_net] is adapter: - if _debug: NetworkServiceElement._debug(" - same network") - return - - # build a response - iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource - - # send it back - self.response(adapter, iamrtn) + break - else: - if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + # found a path + if router_info: + if _debug: NetworkServiceElement._debug(" - router found: %r", router_info) - # build a request - whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) - whoisrtn.pduDestination = LocalBroadcast() + if snet_adapter is adapter: + if _debug: NetworkServiceElement._debug(" - same network") + return - # if the request had a source, forward it along - if npdu.npduSADR: - whoisrtn.npduSADR = npdu.npduSADR - else: - whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) - if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it to all of the (other) 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) + # send it back + self.response(adapter, iamrtn) + + else: + if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + + # build a request + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) + whoisrtn.pduDestination = LocalBroadcast() + + # if the request had a source, forward it along + if npdu.npduSADR: + whoisrtn.npduSADR = npdu.npduSADR + else: + whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) + if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + + # send it to all of the (other) 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) def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) @@ -895,7 +966,7 @@ def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - sap: %r", sap) # pass along to the service access point - sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + sap.update_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) # skip if this is not a router if len(sap.adapters) == 1: @@ -1093,6 +1164,9 @@ def NetworkNumberIs(self, adapter, npdu): if adapter.adapterNet is None: if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + # update the routing information + sap.router_info_cache.update_source_network(None, npdu.nniNet) + # delete the reference from an unknown network del sap.adapters[None] @@ -1103,7 +1177,6 @@ def NetworkNumberIs(self, adapter, npdu): sap.adapters[adapter.adapterNet] = adapter if _debug: NetworkServiceElement._debug(" - local network learned") - ###TODO: s/None/net/g in routing tables return # check if this matches what we have @@ -1118,6 +1191,9 @@ def NetworkNumberIs(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - learning something new") + # update the routing information + sap.router_info_cache.update_source_network(adapter.adapterNet, npdu.nniNet) + # delete the reference from the old (learned) network del sap.adapters[adapter.adapterNet] @@ -1127,7 +1203,4 @@ def NetworkNumberIs(self, adapter, npdu): # we now know what network this is sap.adapters[adapter.adapterNet] = adapter - ###TODO: s/old/new/g in routing tables - bacpypes_debugging(NetworkServiceElement) - diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 0254175a..c344e901 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -330,6 +330,8 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False arry[arrayIndex] = value except IndexError: raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + except TypeError: + raise ExecutionError(errorClass='property', errorCode='valueOutOfRange') # check for monitors, call each one with the old and new value if is_monitored: @@ -379,7 +381,7 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr class OptionalProperty(StandardProperty, Logging): - """The property is required to be present and readable using BACnet services.""" + """The property is optional and need not be present.""" def __init__(self, identifier, datatype, default=None, optional=True, mutable=False): if _debug: @@ -453,6 +455,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class Object(Logging): _debug_contents = ('_app',) + _object_supports_cov = False properties = \ [ ObjectIdentifierProperty('objectIdentifier', ObjectIdentifier, optional=False) @@ -460,6 +463,9 @@ class Object(Logging): , OptionalProperty('description', CharacterString) , OptionalProperty('profileName', CharacterString) , ReadableProperty('propertyList', ArrayOf(PropertyIdentifier)) + , OptionalProperty('tags', ArrayOf(NameValue)) + , OptionalProperty('profileLocation', CharacterString) + , OptionalProperty('profileName', CharacterString) ] _properties = {} @@ -723,6 +729,8 @@ class AccessCredentialObject(Object): class AccessDoorObject(Object): objectType = 'accessDoor' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', DoorValue) , ReadableProperty('statusFlags', StatusFlags) @@ -761,6 +769,8 @@ class AccessDoorObject(Object): class AccessPointObject(Object): objectType = 'accessPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('eventState', EventState) @@ -946,6 +956,8 @@ class AlertEnrollmentObject(Object): class AnalogInputObject(Object): objectType = 'analogInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -982,6 +994,8 @@ class AnalogInputObject(Object): class AnalogOutputObject(Object): objectType = 'analogOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -1019,6 +1033,8 @@ class AnalogOutputObject(Object): class AnalogValueObject(Object): objectType = 'analogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1073,6 +1089,8 @@ class AveragingObject(Object): class BinaryInputObject(Object): objectType = 'binaryInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1108,6 +1126,8 @@ class BinaryInputObject(Object): class BinaryOutputObject(Object): objectType = 'binaryOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1147,6 +1167,8 @@ class BinaryOutputObject(Object): class BinaryValueObject(Object): objectType = 'binaryValue' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , ReadableProperty('statusFlags',StatusFlags) @@ -1251,6 +1273,8 @@ class ChannelObject(Object): class CharacterStringValueObject(Object): objectType = 'characterstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -1292,6 +1316,8 @@ class CommandObject(Object): class CredentialDataInputObject(Object): objectType = 'credentialDataInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', AuthenticationFactor) , ReadableProperty('statusFlags', StatusFlags) @@ -1315,6 +1341,8 @@ class CredentialDataInputObject(Object): class DatePatternValueObject(Object): objectType = 'datePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1329,6 +1357,8 @@ class DatePatternValueObject(Object): class DateValueObject(Object): objectType = 'dateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1343,6 +1373,8 @@ class DateValueObject(Object): class DateTimePatternValueObject(Object): objectType = 'datetimePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1358,6 +1390,8 @@ class DateTimePatternValueObject(Object): class DateTimeValueObject(Object): objectType = 'datetimeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1564,6 +1598,8 @@ class GroupObject(Object): class IntegerValueObject(Object): objectType = 'integerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Integer) , ReadableProperty('statusFlags', StatusFlags) @@ -1600,6 +1636,8 @@ class IntegerValueObject(Object): class LargeAnalogValueObject(Object): objectType = 'largeAnalogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Double) , ReadableProperty('statusFlags', StatusFlags) @@ -1636,6 +1674,8 @@ class LargeAnalogValueObject(Object): class LifeSafetyPointObject(Object): objectType = 'lifeSafetyPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1675,6 +1715,8 @@ class LifeSafetyPointObject(Object): class LifeSafetyZoneObject(Object): objectType = 'lifeSafetyZone' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1712,6 +1754,8 @@ class LifeSafetyZoneObject(Object): class LightingOutputObject(Object): objectType = 'lightingOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , ReadableProperty('trackingValue', Real) @@ -1743,6 +1787,8 @@ class LightingOutputObject(Object): class LoadControlObject(Object): objectType = 'loadControl' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', ShedState) , OptionalProperty('stateDescription', CharacterString) @@ -1778,6 +1824,8 @@ class LoadControlObject(Object): class LoopObject(Object): objectType = 'loop' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1825,6 +1873,8 @@ class LoopObject(Object): class MultiStateInputObject(Object): objectType = 'multiStateInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1855,6 +1905,8 @@ class MultiStateInputObject(Object): class MultiStateOutputObject(Object): objectType = 'multiStateOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1886,6 +1938,8 @@ class MultiStateOutputObject(Object): class MultiStateValueObject(Object): objectType = 'multiStateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -2038,6 +2092,8 @@ class NotificationForwarderObject(Object): class OctetStringValueObject(Object): objectType = 'octetstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -2052,6 +2108,8 @@ class OctetStringValueObject(Object): class PositiveIntegerValueObject(Object): objectType = 'positiveIntegerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -2113,6 +2171,8 @@ class ProgramObject(Object): class PulseConverterObject(Object): objectType = 'pulseConverter' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('inputReference', ObjectPropertyReference) @@ -2190,6 +2250,8 @@ class StructuredViewObject(Object): class TimePatternValueObject(Object): objectType = 'timePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) @@ -2204,6 +2266,8 @@ class TimePatternValueObject(Object): class TimeValueObject(Object): objectType = 'timeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 668c99ac..a32ba6b7 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -612,6 +612,14 @@ def __init__(self, arg=None): if not self.is_valid(arg): raise ValueError("value out of range") self.value = arg + elif isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + raise TypeError("invalid constructor datatype") + if not self.is_valid(arg): + raise ValueError("value out of range") + self.value = arg elif isinstance(arg, Unsigned): if not self.is_valid(arg.value): raise ValueError("value out of range") @@ -647,7 +655,12 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - if not isinstance(arg, (int, long)) or isinstance(arg, bool): + if isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + return False + elif not isinstance(arg, (int, long)) or isinstance(arg, bool): return False if (arg < cls._low_limit): return False @@ -1652,6 +1665,7 @@ class ObjectType(Enumerated): , 'timeValue':50 , 'trendLog':20 , 'trendLogMultiple':27 + , 'networkPort':56 } expand_enumerations(ObjectType) diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index fc631733..00a9a037 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -5,7 +5,7 @@ from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array, ArrayOf +from ..constructeddata import Any, Array, ArrayOf, List from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ @@ -71,6 +71,8 @@ def do_ReadPropertyRequest(self, apdu): elif not isinstance(value, datatype.subtype): raise TypeError("invalid result datatype, expecting %r and got %r" \ % (datatype.subtype.__name__, type(value).__name__)) + elif issubclass(datatype, List): + value = datatype(value) elif not isinstance(value, datatype): raise TypeError("invalid result datatype, expecting %r and got %r" \ % (datatype.__name__, type(value).__name__)) @@ -291,6 +293,11 @@ def do_ReadPropertyMultipleRequest(self, apdu): for propId, prop in obj._properties.items(): if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + # skip propertyList for ReadPropertyMultiple + if (propId == 'propertyList'): + if _debug: ReadWritePropertyMultipleServices._debug(" - ignore propertyList") + continue + if (propertyIdentifier == 'all'): pass elif (propertyIdentifier == 'required') and (prop.optional): diff --git a/py25/bacpypes/settings.py b/py25/bacpypes/settings.py new file mode 100644 index 00000000..ba89b099 --- /dev/null +++ b/py25/bacpypes/settings.py @@ -0,0 +1,95 @@ +#!/usr/bin/python + +""" +Settings +""" + +import os + + +class Settings(dict): + """ + Settings + """ + + def __getattr__(self, name): + if name not in self: + raise AttributeError("No such setting: " + name) + return self[name] + + def __setattr__(self, name, value): + if name not in self: + raise AttributeError("No such setting: " + name) + self[name] = value + + +# globals +settings = Settings( + debug=set(), + color=False, + debug_file="", + max_bytes=1048576, + backup_count=5, + route_aware=False, +) + + +def os_settings(): + """ + Update the settings from known OS environment variables. + """ + for setting_name, env_name in ( + ("debug", "BACPYPES_DEBUG"), + ("color", "BACPYPES_COLOR"), + ("debug_file", "BACPYPES_DEBUG_FILE"), + ("max_bytes", "BACPYPES_MAX_BYTES"), + ("backup_count", "BACPYPES_BACKUP_COUNT"), + ("route_aware", "BACPYPES_ROUTE_AWARE"), + ): + env_value = os.getenv(env_name, None) + if env_value is not None: + cur_value = settings[setting_name] + + if isinstance(cur_value, bool): + env_value = env_value.lower() + if env_value in ("set", "true"): + env_value = True + elif env_value in ("reset", "false"): + env_value = False + else: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, int): + try: + env_value = int(env_value) + except: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, str): + pass + elif isinstance(cur_value, list): + env_value = env_value.split() + elif isinstance(cur_value, set): + env_value = set(env_value.split()) + else: + raise TypeError("setting type: " + setting_name) + settings[setting_name] = env_value + + +def dict_settings(**kwargs): + """ + Update the settings from key/value content. Lists are morphed into sets + if necessary, giving a setting any value is acceptable if there isn't one + already set, otherwise protect against setting type changes. + """ + for setting_name, kw_value in kwargs.items(): + cur_value = settings.get(setting_name, None) + + if cur_value is None: + pass + elif isinstance(cur_value, set): + if isinstance(kw_value, list): + kw_value = set(kw_value) + elif not isinstance(kw_value, set): + raise TypeError(setting_name) + elif not isinstance(kw_value, type(cur_value)): + raise TypeError("setting type: " + setting_name) + settings[setting_name] = kw_value diff --git a/py25/bacpypes/udp.py b/py25/bacpypes/udp.py index 2ea59ddc..89ebb02d 100755 --- a/py25/bacpypes/udp.py +++ b/py25/bacpypes/udp.py @@ -152,7 +152,12 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non self.set_reuse_addr() # proceed with the bind - self.bind(address) + try: + self.bind(address) + except socket.error, err: + if _debug: UDPDirector._debug(" - bind error: %r", err) + self.close() + raise if _debug: UDPDirector._debug(" - getsockname: %r", self.socket.getsockname()) # allow it to send broadcasts diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index e4bed942..9e13daa5 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,10 +18,12 @@ # Project Metadata # -__version__ = '0.17.6' +__version__ = '0.17.7' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' +from . import settings + # # Communications Core Modules # diff --git a/py27/bacpypes/analysis.py b/py27/bacpypes/analysis.py index c13ff455..d020c66e 100755 --- a/py27/bacpypes/analysis.py +++ b/py27/bacpypes/analysis.py @@ -23,6 +23,7 @@ except: pass +from .settings import settings from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address @@ -216,7 +217,10 @@ def decode_packet(data): # lift the address for forwarded NPDU's if atype is ForwardedNPDU: + old_pdu_source = pdu.pduSource pdu.pduSource = bpdu.bvlciAddress + if settings.route_aware: + pdu.pduSource.addrRoute = old_pdu_source # no deeper decoding for some elif atype not in (DistributeBroadcastToNetwork, OriginalUnicastNPDU, OriginalBroadcastNPDU): return pdu @@ -254,6 +258,8 @@ def decode_packet(data): # "lift" the source and destination address if npdu.npduSADR: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + apdu.pduSource.addrRoute = npdu.pduSource else: apdu.pduSource = npdu.pduSource if npdu.npduDADR: diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index 3062bd71..d18411f0 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -1575,11 +1575,12 @@ class ReinitializeDeviceRequestReinitializedStateOfDevice(Enumerated): enumerations = \ { 'coldstart':0 , 'warmstart':1 - , 'startbackup':2 - , 'endbackup':3 - , 'startrestore':4 - , 'endrestore':5 - , 'abortrestore':6 + , 'startBackup':2 + , 'endBackup':3 + , 'startRestore':4 + , 'endRestore':5 + , 'abortRestore':6 + , 'activateChanges':7 } class ReinitializeDeviceRequest(ConfirmedRequestSequence): diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index c3f3426a..b3bc96ee 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -7,6 +7,8 @@ import warnings from .debugging import bacpypes_debugging, DebugContents, ModuleLogger + +from .core import deferred from .comm import ApplicationServiceElement, bind from .iocb import IOController, SieveQueue @@ -209,6 +211,8 @@ def release(self, device_info): @bacpypes_debugging class Application(ApplicationServiceElement, Collector): + _startup_disabled = False + def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, aseID=None): if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) ApplicationServiceElement.__init__(self, aseID) @@ -250,6 +254,12 @@ def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, as # now set up the rest of the capabilities Collector.__init__(self) + # if starting up is enabled, find all the startup functions + if not self._startup_disabled: + for fn in self.capability_functions('startup'): + if _debug: Application._debug(" - startup fn: %r" , fn) + deferred(fn, self) + def add_object(self, obj): """Add an object to the local collection.""" if _debug: Application._debug("add_object %r", obj) @@ -616,7 +626,7 @@ def __init__(self, localAddress, bbmdAddress=None, bbmdTTL=None, eID=None): else: self.bip = BIPForeign(bbmdAddress, bbmdTTL) self.annexj = AnnexJCodec() - self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True) + self.mux = UDPMultiplexer(self.localAddress, noBroadcast=False) # bind the bottom layers bind(self.bip, self.annexj, self.mux.annexJ) diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index c0d61d4a..c021282f 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -516,7 +516,6 @@ def segmented_request(self, apdu): if _debug: ClientSSM._debug(" - error/reject/abort") self.set_state(COMPLETED) - self.response = apdu self.response(apdu) else: diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index 6e5b75e5..1bc45450 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -4,11 +4,12 @@ Base Types """ -from .debugging import ModuleLogger +from .debugging import bacpypes_debugging, ModuleLogger +from .errors import MissingRequiredParameter -from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ +from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned, Unsigned16 + Unsigned, Unsigned16, Tag from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -1716,12 +1717,88 @@ class RouterEntry(Sequence): , Element('status', RouterEntryStatus) # Defined Above ] +@bacpypes_debugging class NameValue(Sequence): sequenceElements = \ [ Element('name', CharacterString) - , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + , Element('value', AnyAtomic, None, True) ] + def __init__(self, name=None, value=None): + if _debug: NameValue._debug("__init__ name=%r value=%r", name, value) + + # default to no value + self.name = name + self.value = None + + if value is None: + pass + elif isinstance(value, (Atomic, DateTime)): + self.value = value + elif isinstance(value, Tag): + self.value = value.app_to_object() + else: + raise TypeError("invalid constructor datatype") + + def encode(self, taglist): + if _debug: NameValue._debug("(%r)encode %r", self.__class__.__name__, taglist) + + # build a tag and encode the name into it + tag = Tag() + CharacterString(self.name).encode(tag) + taglist.append(tag.app_to_context(0)) + + # the value is optional + if self.value is not None: + if isinstance(self.value, DateTime): + # has its own encoder + self.value.encode(taglist) + else: + # atomic values encode into a tag + tag = Tag() + self.value.encode(tag) + taglist.append(tag) + + def decode(self, taglist): + if _debug: NameValue._debug("(%r)decode %r", self.__class__.__name__, taglist) + + # no contents yet + self.name = None + self.value = None + + # look for the context encoded character string + tag = taglist.Peek() + if _debug: NameValue._debug(" - name tag: %r", tag) + if (tag is None) or (tag.tagClass != Tag.contextTagClass) or (tag.tagNumber != 0): + raise MissingRequiredParameter("%s is a missing required element of %s" % ('name', self.__class__.__name__)) + + # pop it off and save the value + taglist.Pop() + tag = tag.context_to_app(Tag.characterStringAppTag) + self.name = CharacterString(tag).value + + # look for the optional application encoded value + tag = taglist.Peek() + if _debug: NameValue._debug(" - value tag: %r", tag) + if tag and (tag.tagClass == Tag.applicationTagClass): + + # if it is a date check the next one for a time + if (tag.tagNumber == Tag.dateAppTag) and (len(taglist.tagList) >= 2): + next_tag = taglist.tagList[1] + if _debug: NameValue._debug(" - next_tag: %r", next_tag) + + if (next_tag.tagClass == Tag.applicationTagClass) and (next_tag.tagNumber == Tag.timeAppTag): + if _debug: NameValue._debug(" - remaining tag list 0: %r", taglist.tagList) + + self.value = DateTime() + self.value.decode(taglist) + if _debug: NameValue._debug(" - date time value: %r", self.value) + + # just a primitive value + if self.value is None: + taglist.Pop() + self.value = tag.app_to_object() + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 558ac684..99f99981 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -6,8 +6,8 @@ import sys import struct -from time import time as _time +from .settings import settings from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .udp import UDPDirector @@ -90,6 +90,7 @@ def __init__(self, addr=None, noBroadcast=False): UDPMultiplexer._debug(" - address: %r", self.address) UDPMultiplexer._debug(" - addrTuple: %r", self.addrTuple) UDPMultiplexer._debug(" - addrBroadcastTuple: %r", self.addrBroadcastTuple) + UDPMultiplexer._debug(" - route_aware: %r", settings.route_aware) # create and bind the direct address self.direct = _MultiplexClient(self) @@ -100,7 +101,7 @@ def __init__(self, addr=None, noBroadcast=False): if specialBroadcast and (not noBroadcast) and sys.platform in ('linux2', 'darwin'): self.broadcast = _MultiplexClient(self) self.broadcastPort = UDPDirector(self.addrBroadcastTuple, reuse=True) - bind(self.direct, self.broadcastPort) + bind(self.broadcast, self.broadcastPort) else: self.broadcast = None self.broadcastPort = None @@ -120,7 +121,7 @@ def close_socket(self): def indication(self, server, pdu): if _debug: UDPMultiplexer._debug("indication %r %r", server, pdu) - # check for a broadcast message + # broadcast message if pdu.pduDestination.addrType == Address.localBroadcastAddr: dest = self.addrBroadcastTuple if _debug: UDPMultiplexer._debug(" - requesting local broadcast: %r", dest) @@ -129,6 +130,7 @@ def indication(self, server, pdu): if not dest: return + # unicast message elif pdu.pduDestination.addrType == Address.localStationAddr: dest = unpack_ip_addr(pdu.pduDestination.addrAddr) if _debug: UDPMultiplexer._debug(" - requesting local station: %r", dest) diff --git a/py27/bacpypes/consolelogging.py b/py27/bacpypes/consolelogging.py index 93fc3f43..aa338d8c 100755 --- a/py27/bacpypes/consolelogging.py +++ b/py27/bacpypes/consolelogging.py @@ -6,10 +6,12 @@ import os import sys +import json import logging import logging.handlers import argparse +from .settings import Settings, settings, os_settings, dict_settings from .debugging import bacpypes_debugging, LoggingFormatter, ModuleLogger from ConfigParser import ConfigParser as _ConfigParser @@ -18,13 +20,6 @@ _debug = 0 _log = ModuleLogger(globals()) -# configuration -BACPYPES_INI = os.getenv('BACPYPES_INI', 'BACpypes.ini') -BACPYPES_DEBUG = os.getenv('BACPYPES_DEBUG', '') -BACPYPES_COLOR = os.getenv('BACPYPES_COLOR', None) -BACPYPES_MAXBYTES = int(os.getenv('BACPYPES_MAXBYTES', 1048576)) -BACPYPES_BACKUPCOUNT = int(os.getenv('BACPYPES_BACKUPCOUNT', 5)) - # # ConsoleLogHandler # @@ -83,6 +78,7 @@ class ArgumentParser(argparse.ArgumentParser): --buggers list the debugging logger names --debug [DEBUG [DEBUG ...]] attach a handler to loggers --color debug in color + --route-aware turn on route aware """ def __init__(self, **kwargs): @@ -90,6 +86,10 @@ def __init__(self, **kwargs): if _debug: ArgumentParser._debug("__init__") argparse.ArgumentParser.__init__(self, **kwargs) + # load settings from the environment + self.update_os_env() + if _debug: ArgumentParser._debug(" - os environment") + # add a way to get a list of the debugging hooks self.add_argument("--buggers", help="list the debugging logger names", @@ -105,8 +105,24 @@ def __init__(self, **kwargs): self.add_argument("--color", help="turn on color debugging", action="store_true", + default=None, + ) + + # add a way to turn on route aware + self.add_argument("--route-aware", + help="turn on route aware", + action="store_true", + default=None, ) + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: ArgumentParser._debug("update_os_env") + + # use settings function + os_settings() + if _debug: ArgumentParser._debug(" - settings: %r", settings) + def parse_args(self, *args, **kwargs): """Parse the arguments as usual, then add default processing.""" if _debug: ArgumentParser._debug("parse_args") @@ -114,6 +130,51 @@ def parse_args(self, *args, **kwargs): # pass along to the parent class result_args = argparse.ArgumentParser.parse_args(self, *args, **kwargs) + # update settings + self.expand_args(result_args) + if _debug: ArgumentParser._debug(" - args expanded") + + # add debugging loggers + self.interpret_debugging(result_args) + if _debug: ArgumentParser._debug(" - interpreted debugging") + + # return what was parsed and expanded + return result_args + + def expand_args(self, result_args): + """Expand the arguments and/or update the settings.""" + if _debug: ArgumentParser._debug("expand_args %r", result_args) + + # check for debug + if result_args.debug is None: + if _debug: ArgumentParser._debug(" - debug not specified") + elif not result_args.debug: + if _debug: ArgumentParser._debug(" - debug with no args") + settings.debug.update(["__main__"]) + else: + if _debug: ArgumentParser._debug(" - debug: %r", result_args.debug) + settings.debug.update(result_args.debug) + + # check for color + if result_args.color is None: + if _debug: ArgumentParser._debug(" - color not specified") + else: + if _debug: ArgumentParser._debug(" - color: %r", result_args.color) + settings.color = result_args.color + + # check for route aware + if result_args.route_aware is None: + if _debug: ArgumentParser._debug(" - route_aware not specified") + else: + if _debug: ArgumentParser._debug(" - route_aware: %r", result_args.route_aware) + settings.route_aware = result_args.route_aware + + def interpret_debugging(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: + ArgumentParser._debug("interpret_debugging %r", result_args) + ArgumentParser._debug(" - settings: %r", settings) + # check to dump labels if result_args.buggers: loggers = sorted(logging.Logger.manager.loggerDict.keys()) @@ -121,47 +182,37 @@ def parse_args(self, *args, **kwargs): sys.stdout.write(loggerName + '\n') sys.exit(0) - # check for debug - if result_args.debug is None: - # --debug not specified - result_args.debug = [] - elif not result_args.debug: - # --debug, but no arguments - result_args.debug = ["__main__"] - - # check for debugging from the environment - if BACPYPES_DEBUG: - result_args.debug.extend(BACPYPES_DEBUG.split()) - if BACPYPES_COLOR: - result_args.color = True - # keep track of which files are going to be used file_handlers = {} # loop through the bug list - for i, debug_name in enumerate(result_args.debug): - color = (i % 6) + 2 if result_args.color else None + for i, debug_name in enumerate(settings.debug): + color = (i % 6) + 2 if settings.color else None debug_specs = debug_name.split(':') - if len(debug_specs) == 1: + if (len(debug_specs) == 1) and (not settings.debug_file): ConsoleLogHandler(debug_name, color=color) else: # the debugger name is just the first component - debug_name = debug_specs[0] + debug_name = debug_specs.pop(0) + + if debug_specs: + file_name = debug_specs.pop(0) + else: + file_name = settings.debug_file # if the file is already being used, use the already created handler - file_name = debug_specs[1] if file_name in file_handlers: handler = file_handlers[file_name] else: - if len(debug_specs) >= 3: - maxBytes = int(debug_specs[2]) + if debug_specs: + maxBytes = int(debug_specs.pop(0)) else: - maxBytes = BACPYPES_MAXBYTES - if len(debug_specs) >= 4: - backupCount = int(debug_specs[3]) + maxBytes = settings.max_bytes + if debug_specs: + backupCount = int(debug_specs.pop(0)) else: - backupCount = BACPYPES_BACKUPCOUNT + backupCount = settings.backup_count # create a handler handler = logging.handlers.RotatingFileHandler( @@ -187,7 +238,7 @@ class ConfigArgumentParser(ArgumentParser): """ ConfigArgumentParser extends the ArgumentParser with the functionality to - read in a configuration file. + read in an INI configuration file. --ini INI provide a separate INI file """ @@ -200,15 +251,22 @@ def __init__(self, **kwargs): # add a way to read a configuration file self.add_argument('--ini', help="device object configuration file", - default=BACPYPES_INI, + default=settings.ini, ) - def parse_args(self, *args, **kwargs): - """Parse the arguments as usual, then add default processing.""" - if _debug: ConfigArgumentParser._debug("parse_args") + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: ConfigArgumentParser._debug("update_os_env") - # pass along to the parent class - result_args = ArgumentParser.parse_args(self, *args, **kwargs) + # start with normal env vars + ArgumentParser.update_os_env(self) + + # provide a default value for the INI file name + settings["ini"] = os.getenv("BACPYPES_INI", "BACpypes.ini") + + def expand_args(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: ConfigArgumentParser._debug("expand_args %r", result_args) # read in the configuration file config = _ConfigParser() @@ -220,12 +278,85 @@ def parse_args(self, *args, **kwargs): raise RuntimeError("INI file with BACpypes section required") # convert the contents to an object - ini_obj = type('ini', (object,), dict(config.items('BACpypes'))) + ini_obj = Settings(dict(config.items('BACpypes'))) if _debug: _log.debug(" - ini_obj: %r", ini_obj) # add the object to the parsed arguments setattr(result_args, 'ini', ini_obj) - # return what was parsed - return result_args + # continue with normal expansion + ArgumentParser.expand_args(self, result_args) + +# +# JSONArgumentParser +# + +def _deunicodify_hook(pairs): + """ + JSON decoding hook to eliminate unicode strings in keys and values. + """ + new_pairs = [] + for key, value in pairs: + if isinstance(value, unicode): + value = value.encode('utf-8') + elif isinstance(value, list): + value = [v.encode('utf-8') if isinstance(v, unicode) else v for v in value] + if isinstance(key, unicode): + key = key.encode('utf-8') + new_pairs.append((key, value)) + return Settings(new_pairs) + + +@bacpypes_debugging +class JSONArgumentParser(ArgumentParser): + + """ + JSONArgumentParser extends the ArgumentParser with the functionality to + read in a JSON configuration file. + + --json JSON provide a separate JSON file + """ + + def __init__(self, **kwargs): + """Follow normal initialization and add BACpypes arguments.""" + if _debug: JSONArgumentParser._debug("__init__") + ArgumentParser.__init__(self, **kwargs) + + # add a way to read a configuration file + self.add_argument('--json', + help="configuration file", + default=settings.json, + ) + + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: JSONArgumentParser._debug("update_os_env") + + # start with normal env vars + ArgumentParser.update_os_env(self) + + # provide a default value for the INI file name + settings["json"] = os.getenv("BACPYPES_JSON", "BACpypes.json") + + def expand_args(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: JSONArgumentParser._debug("expand_args %r", result_args) + + # read in the settings file + try: + with open(result_args.json) as json_file: + json_obj = json.load(json_file, object_pairs_hook=_deunicodify_hook) + if _debug: JSONArgumentParser._debug(" - json_obj: %r", json_obj) + except IOError: + raise RuntimeError("settings file not found: %r\n" % (settings.json,)) + + # look for settings + if "bacpypes" in json_obj: + dict_settings(**json_obj.bacpypes) + if _debug: JSONArgumentParser._debug(" - settings: %r", settings) + + # add the object to the parsed arguments + setattr(result_args, 'json', json_obj) + # continue with normal expansion + ArgumentParser.expand_args(self, result_args) diff --git a/py27/bacpypes/local/file.py b/py27/bacpypes/local/file.py index 8007bef4..6afe8495 100644 --- a/py27/bacpypes/local/file.py +++ b/py27/bacpypes/local/file.py @@ -1,16 +1,9 @@ #!/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()) diff --git a/py27/bacpypes/local/object.py b/py27/bacpypes/local/object.py index 3078b9c6..b789ce28 100644 --- a/py27/bacpypes/local/object.py +++ b/py27/bacpypes/local/object.py @@ -1,12 +1,26 @@ #!/usr/bin/env python +import re + from ..debugging import bacpypes_debugging, ModuleLogger -from ..basetypes import PropertyIdentifier -from ..constructeddata import ArrayOf +from ..task import OneShotTask +from ..primitivedata import Atomic, Null, BitString, CharacterString, \ + Date, Integer, Double, Enumerated, OctetString, Real, Time, Unsigned +from ..basetypes import PropertyIdentifier, DateTime, NameValue, BinaryPV, \ + ChannelValue, DoorValue, PriorityValue, PriorityArray +from ..constructeddata import Array, ArrayOf, SequenceOf from ..errors import ExecutionError -from ..object import Property, Object +from ..object import Property, ReadableProperty, WritableProperty, OptionalProperty, Object, \ + AccessDoorObject, AnalogOutputObject, AnalogValueObject, \ + BinaryOutputObject, BinaryValueObject, BitStringValueObject, CharacterStringValueObject, \ + DateValueObject, DatePatternValueObject, DateTimePatternValueObject, \ + DateTimeValueObject, IntegerValueObject, \ + LargeAnalogValueObject, LightingOutputObject, MultiStateOutputObject, \ + MultiStateValueObject, OctetStringValueObject, PositiveIntegerValueObject, \ + TimeValueObject, TimePatternValueObject, ChannelObject + # some debugging _debug = 0 @@ -66,3 +80,804 @@ class CurrentPropertyListMixIn(Object): CurrentPropertyList(), ] +# +# Turtle Reference Patterns +# + +# character reference patterns +HEX = u"[0-9A-Fa-f]" +PERCENT = u"%" + HEX + HEX +UCHAR = u"[\\\]u" + HEX * 4 + "|" + u"[\\\]U" + HEX * 8 + +# character sets +PN_CHARS_BASE = ( + u"A-Za-z" + u"\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF" + u"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + u"\uFDF0-\uFFFD\U00010000-\U000EFFFF" +) + +PN_CHARS_U = PN_CHARS_BASE + u"_" +PN_CHARS = u"-" + PN_CHARS_U + u"0-9\u00B7\u0300-\u036F\u203F-\u2040" + +# patterns +IRIREF = u'[<]([^\u0000-\u0020<>"{}|^`\\\]|' + UCHAR + u")*[>]" +PN_PREFIX = u"[" + PN_CHARS_BASE + u"](([." + PN_CHARS + u"])*[" + PN_CHARS + u"])?" + +PN_LOCAL_ESC = u"[-\\_~.!$&'()*+,;=/?#@%]" +PLX = u"(" + PERCENT + u"|" + PN_LOCAL_ESC + u")" + +# non-prefixed names +PN_LOCAL = ( + u"([" + + PN_CHARS_U + + u":0-9]|" + + PLX + + u")(([" + + PN_CHARS + + u".:]|" + + PLX + + u")*([" + + PN_CHARS + + u":]|" + + PLX + + u"))?" +) + +# namespace prefix declaration +PNAME_NS = u"(" + PN_PREFIX + u")?:" + +# prefixed names +PNAME_LN = PNAME_NS + PN_LOCAL + +# blank nodes +BLANK_NODE_LABEL = ( + u"_:[" + PN_CHARS_U + u"0-9]([" + PN_CHARS + u".]*[" + PN_CHARS + u"])?" +) + +# see https://www.w3.org/TR/turtle/#sec-parsing-terms +iriref_re = re.compile(u"^" + IRIREF + u"$", re.UNICODE) +local_name_re = re.compile(u"^" + PN_LOCAL + u"$", re.UNICODE) +namespace_prefix_re = re.compile(u"^" + PNAME_NS + u"$", re.UNICODE) +prefixed_name_re = re.compile(u"^" + PNAME_LN + u"$", re.UNICODE) +blank_node_re = re.compile(u"^" + BLANK_NODE_LABEL + u"$", re.UNICODE) + +# see https://tools.ietf.org/html/bcp47#section-2.1 for better syntax +language_tag_re = re.compile(u"^[A-Za-z0-9-]+$", re.UNICODE) + +class IRI: + # regex from RFC 3986 + _e = r"^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?" + _p = re.compile(_e) + _default_ports = (("http", ":80"), ("https", ":443")) + + def __init__(self, iri=None): + self.iri = iri + + if not iri: + g = (None, None, None, None, None) + else: + m = IRI._p.match(iri) + if not m: + raise ValueError("not an IRI") + + # remove default http and https ports + g = list(m.groups()) + for scheme, suffix in IRI._default_ports: + if (g[0] == scheme) and g[1] and g[1].endswith(suffix): + g[1] = g[1][: g[1].rfind(":")] + break + + self.scheme, self.authority, self.path, self.query, self.fragment = g + + def __str__(self): + rval = "" + if self.scheme: + rval += self.scheme + ":" + if self.authority is not None: + rval += "//" + self.authority + if self.path is not None: + rval += self.path + if self.query is not None: + rval += "?" + self.query + if self.fragment is not None: + rval += "#" + self.fragment + return rval + + def is_local_name(self): + if not all( + ( + self.scheme is None, + self.authority is None, + self.path, + self.query is None, + self.fragment is None, + ) + ): + return False + if self.path.startswith(":") or "/" in self.path: # term is not ':x' + return False + return True + + def is_prefix(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path == "" # term is 'x:' + else: + return self.path == ":" # term is ':' + + def is_prefixed_name(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path != "" # term is 'x:y' + else: # term is ':y' but not ':' + return self.path and (self.path != ":") and self.path.startswith(":") + + def resolve(self, iri): + """Resolve a relative IRI to this IRI as a base.""" + # parse the IRI if necessary + if isinstance(iri, str): + iri = IRI(iri) + elif not isinstance(iri, IRI): + raise TypeError("iri must be an IRI or a string") + + # return an IRI object + rslt = IRI() + + if iri.scheme and iri.scheme != self.scheme: + rslt.scheme = iri.scheme + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.scheme = self.scheme + + if iri.authority is not None: + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.authority = self.authority + + if not iri.path: + rslt.path = self.path + if iri.query is not None: + rslt.query = iri.query + else: + rslt.query = self.query + else: + if iri.path.startswith("/"): + # IRI represents an absolute path + rslt.path = iri.path + else: + # merge paths + path = self.path + + # append relative path to the end of the last + # directory from base + path = path[0 : path.rfind("/") + 1] + if len(path) > 0 and not path.endswith("/"): + path += "/" + path += iri.path + + rslt.path = path + + rslt.query = iri.query + + # normalize path + if rslt.path != "": + rslt.remove_dot_segments() + + rslt.fragment = iri.fragment + + return rslt + + def remove_dot_segments(self): + # empty path shortcut + if len(self.path) == 0: + return + + input_ = self.path.split("/") + output_ = [] + + while len(input_) > 0: + next = input_.pop(0) + done = len(input_) == 0 + + if next == ".": + if done: + # ensure output has trailing / + output_.append("") + continue + + if next == "..": + if len(output_) > 0: + output_.pop() + if done: + # ensure output has trailing / + output_.append("") + continue + + output_.append(next) + + # ensure output has leading / + if len(output_) > 0 and output_[0] != "": + output_.insert(0, "") + if len(output_) == 1 and output_[0] == "": + return "/" + + self.path = "/".join(output_) + + +@bacpypes_debugging +class TagSet: + def index(self, name, value=None): + """Find the first name with dictionary semantics or (name, value) with + list semantics.""" + if _debug: TagSet._debug("index %r %r", name, value) + + # if this is a NameValue rip it apart first + if isinstance(name, NameValue): + name, value = name.name, name.value + + # no value then look for first matching name + if value is None: + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if name == v.name: + return i + else: + raise KeyError(name) + + # skip int values, it is the zeroth element of an array but does + # not exist for a list + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if ( + name == v.name + and isinstance(value, type(v.value)) + and value.value == v.value.value + ): + return i + else: + raise ValueError((name, value)) + + def add(self, name, value=None): + """Add a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("add %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + # name is a string + if not isinstance(name, str): + raise TypeError("name must be a string, got %r" % (type(name),)) + + # reserved directive names + if name.startswith("@"): + if name == "@base": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@base') + if v and v.value == value.value: + pass + else: + raise ValueError("@base exists") + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + elif name == "@id": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@id') + if v and v.value == value.value: + pass + else: + raise ValueError("@id exists") + +# # check the patterns +# for pattern in (blank_node_re, prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(value.value): +# break +# else: +# raise ValueError("invalid value for @id") + + elif name == "@language": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get("@language") + if v and v.value == value.value: + pass + else: + raise ValueError("@language exists") + + if not language_tag_re.match(value.value): + raise ValueError("value must be a language tag") + + elif name == "@vocab": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@vocab') + if v and v.value == value.value: + pass + else: + raise ValueError("@vocab exists") + + else: + raise ValueError("invalid directive name") + + elif name.endswith(":"): + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get(name) + if v and v.value == value.value: + pass + else: + raise ValueError("prefix exists: %r" % (name,)) + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + else: +# # check the patterns +# for pattern in (prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(name): +# break +# else: +# raise ValueError("invalid name") + pass + + # check the value + if not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # see if the (name, value) already exists + try: + self.index(name, value) + except ValueError: + super(TagSet, self).append(NameValue(name=name, value=value)) + + def discard(self, name, value=None): + """Discard a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("discard %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + indx = self.index(name, value) + return super(TagSet, self).__delitem__(indx) + + def append(self, name_value): + """Override the append operation for mutable set semantics.""" + if _debug: TagSet._debug("append %r", name_value) + + if not isinstance(name_value, NameValue): + raise TypeError + + # turn this into an add operation + self.add(name_value.name, name_value.value) + + def get(self, key, default=None): + """Get the value of a key or default value if the key was not found, + dictionary semantics.""" + if _debug: TagSet._debug("get %r %r", key, default) + + try: + if not isinstance(key, str): + raise TypeError(key) + return self.value[self.index(key)].value + except KeyError: + return default + + def __getitem__(self, item): + """If item is an integer, return the value of the NameValue element + with array/sequence semantics. If the item is a string, return the + value with dictionary semantics.""" + if _debug: TagSet._debug("__getitem__ %r", item) + + # integers imply index + if isinstance(item, int): + return super(TagSet, self).__getitem__(item) + + return self.value[self.index(item)] + + def __setitem__(self, item, value): + """If item is an integer, change the value of the NameValue element + with array/sequence semantics. If the item is a string, change the + current value or add a new value with dictionary semantics.""" + if _debug: TagSet._debug("__setitem__ %r %r", item, value) + + # integers imply index + if isinstance(item, int): + indx = item + if indx < 0: + raise IndexError("assignment index out of range") + elif isinstance(self, Array): + if indx == 0 or indx > len(self.value): + raise IndexError + elif indx >= len(self.value): + raise IndexError + elif isinstance(item, str): + try: + indx = self.index(item) + except KeyError: + self.add(item, value) + return + else: + raise TypeError(repr(item)) + + # check the value + if value is None: + value = Null() + elif not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # now we're good to go + self.value[indx].value = value + + def __delitem__(self, item): + """If the item is a integer, delete the element with array semantics, or + if the item is a string, delete the element with dictionary semantics, + or (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("__delitem__ %r", item) + + # integers imply index + if isinstance(item, int): + indx = item + elif isinstance(item, str): + indx = self.index(item) + elif isinstance(item, tuple): + indx = self.index(*item) + else: + raise TypeError(item) + + return super(TagSet, self).__delitem__(indx) + + def __contains__(self, key): + if _debug: TagSet._debug("__contains__ %r", key) + + try: + if isinstance(key, tuple): + self.index(*key) + elif isinstance(key, str): + self.index(key) + else: + raise TypeError(key) + + return True + except (KeyError, ValueError): + return False + + +class ArrayOfNameValue(TagSet, ArrayOf(NameValue)): + pass + + +class SequenceOfNameValue(TagSet, SequenceOf(NameValue)): + pass + + +class TagsMixIn(Object): + properties = \ + [ OptionalProperty('tags', ArrayOfNameValue) + ] + + +@bacpypes_debugging +def Commandable(datatype, presentValue='presentValue', priorityArray='priorityArray', relinquishDefault='relinquishDefault'): + if _debug: Commandable._debug("Commandable %r ...", datatype) + + class _Commando(object): + + properties = [ + WritableProperty(presentValue, datatype), + ReadableProperty(priorityArray, PriorityArray), + ReadableProperty(relinquishDefault, datatype), + ] + + _pv_choice = None + + def __init__(self, **kwargs): + super(_Commando, self).__init__(**kwargs) + + # build a default value in case one is needed + default_value = datatype().value + if issubclass(datatype, Enumerated): + default_value = datatype._xlate_table[default_value] + if _debug: Commandable._debug(" - default_value: %r", default_value) + + # see if a present value was provided + if (presentValue not in kwargs): + setattr(self, presentValue, default_value) + + # see if a priority array was provided + if (priorityArray not in kwargs): + setattr(self, priorityArray, PriorityArray()) + + # see if a present value was provided + if (relinquishDefault not in kwargs): + setattr(self, relinquishDefault, default_value) + + def _highest_priority_value(self): + if _debug: Commandable._debug("_highest_priority_value") + + priority_array = getattr(self, priorityArray) + for i in range(1, 17): + priority_value = priority_array[i] + if priority_value.null is None: + if _debug: Commandable._debug(" - found at index: %r", i) + + value = getattr(priority_value, _Commando._pv_choice) + value_source = "###" + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + break + else: + value = getattr(self, relinquishDefault) + value_source = None + + if _debug: Commandable._debug(" - value, value_source: %r, %r", value, value_source) + + # return what you found + return value, value_source + + def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct=False): + if _debug: Commandable._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", property, value, arrayIndex, priority, direct) + + # when writing to the presentValue with a priority + if (property == presentValue): + if _debug: Commandable._debug(" - writing to %s, priority %r", presentValue, priority) + + # default (lowest) priority + if priority is None: + priority = 16 + if _debug: Commandable._debug(" - translate to priority array, index %d", priority) + + # translate to updating the priority array + property = priorityArray + arrayIndex = priority + priority = None + + # update the priority array entry + if (property == priorityArray): + if (arrayIndex is None): + if _debug: Commandable._debug(" - writing entire %s", priorityArray) + + # pass along the request + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + else: + if _debug: Commandable._debug(" - writing to %s, array index %d", priorityArray, arrayIndex) + + # check the bounds + if arrayIndex == 0: + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + if (arrayIndex < 1) or (arrayIndex > 16): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + + # update the specific priorty value element + priority_value = getattr(self, priorityArray)[arrayIndex] + if _debug: Commandable._debug(" - priority_value: %r", priority_value) + + # the null or the choice has to be set, the other clear + if value is (): + if _debug: Commandable._debug(" - write a null") + priority_value.null = value + setattr(priority_value, _Commando._pv_choice, None) + else: + if _debug: Commandable._debug(" - write a value") + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + priority_value.null = None + setattr(priority_value, _Commando._pv_choice, value) + + # look for the highest priority value + value, value_source = self._highest_priority_value() + + # compare with the current value + current_value = getattr(self, presentValue) + if value == current_value: + if _debug: Commandable._debug(" - no present value change") + return + + # turn this into a present value change + property = presentValue + arrayIndex = priority = None + + # allow the request to pass through + if _debug: Commandable._debug(" - super: %r %r arrayIndex=%r priority=%r", property, value, arrayIndex, priority) + + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + + # look up a matching priority value choice + for element in PriorityValue.choiceElements: + if issubclass(datatype, element.klass): + _Commando._pv_choice = element.name + break + else: + _Commando._pv_choice = 'constructedValue' + if _debug: Commandable._debug(" - _pv_choice: %r", _Commando._pv_choice) + + # return the class + return _Commando + +# +# MinOnOffTask +# + +@bacpypes_debugging +class MinOnOffTask(OneShotTask): + + def __init__(self, binary_obj): + if _debug: MinOnOffTask._debug("__init__ %s", repr(binary_obj)) + OneShotTask.__init__(self) + + # save a reference to the object + self.binary_obj = binary_obj + + # listen for changes to the present value + self.binary_obj._property_monitors['presentValue'].append(self.present_value_change) + + def present_value_change(self, old_value, new_value): + if _debug: MinOnOffTask._debug("present_value_change %r %r", old_value, new_value) + + # if there's no value change, skip all this + if old_value == new_value: + if _debug: MinOnOffTask._debug(" - no state change") + return + + # get the minimum on/off time + if new_value == 'inactive': + task_delay = getattr(self.binary_obj, 'minimumOnTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum on: %r", task_delay) + elif new_value == 'active': + task_delay = getattr(self.binary_obj, 'minimumOffTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum off: %r", task_delay) + else: + raise ValueError("unrecognized present value for %r: %r" % (self.binary_obj.objectIdentifier, new_value)) + + # if there's no delay, don't bother + if not task_delay: + if _debug: MinOnOffTask._debug(" - no delay") + return + + # set the value at priority 6 + self.binary_obj.WriteProperty('presentValue', new_value, priority=6) + + # install this to run, if there is a delay + self.install_task(delta=task_delay) + + def process_task(self): + if _debug: MinOnOffTask._debug("process_task(%s)", self.binary_obj.objectName) + + # clear the value at priority 6 + self.binary_obj.WriteProperty('presentValue', (), priority=6) + +# +# MinOnOff +# + +@bacpypes_debugging +class MinOnOff(object): + + def __init__(self, **kwargs): + if _debug: MinOnOff._debug("__init__ ...") + super(MinOnOff, self).__init__(**kwargs) + + # create the timer task + self._min_on_off_task = MinOnOffTask(self) + +# +# Commandable Standard Objects +# + +class AccessDoorCmdObject(Commandable(DoorValue), AccessDoorObject): + pass + +class AnalogOutputCmdObject(Commandable(Real), AnalogOutputObject): + pass + +class AnalogValueCmdObject(Commandable(Real), AnalogValueObject): + pass + +### class BinaryLightingOutputCmdObject(Commandable(Real), BinaryLightingOutputObject): +### pass + +class BinaryOutputCmdObject(Commandable(BinaryPV), MinOnOff, BinaryOutputObject): + pass + +class BinaryValueCmdObject(Commandable(BinaryPV), MinOnOff, BinaryValueObject): + pass + +class BitStringValueCmdObject(Commandable(BitString), BitStringValueObject): + pass + +class CharacterStringValueCmdObject(Commandable(CharacterString), CharacterStringValueObject): + pass + +class DateValueCmdObject(Commandable(Date), DateValueObject): + pass + +class DatePatternValueCmdObject(Commandable(Date), DatePatternValueObject): + pass + +class DateTimeValueCmdObject(Commandable(DateTime), DateTimeValueObject): + pass + +class DateTimePatternValueCmdObject(Commandable(DateTime), DateTimePatternValueObject): + pass + +class IntegerValueCmdObject(Commandable(Integer), IntegerValueObject): + pass + +class LargeAnalogValueCmdObject(Commandable(Double), LargeAnalogValueObject): + pass + +class LightingOutputCmdObject(Commandable(Real), LightingOutputObject): + pass + +class MultiStateOutputCmdObject(Commandable(Unsigned), MultiStateOutputObject): + pass + +class MultiStateValueCmdObject(Commandable(Unsigned), MultiStateValueObject): + pass + +class OctetStringValueCmdObject(Commandable(OctetString), OctetStringValueObject): + pass + +class PositiveIntegerValueCmdObject(Commandable(Unsigned), PositiveIntegerValueObject): + pass + +class TimeValueCmdObject(Commandable(Time), TimeValueObject): + pass + +class TimePatternValueCmdObject(Commandable(Time), TimePatternValueObject): + pass + +@bacpypes_debugging +class ChannelValueProperty(Property): + + def __init__(self): + if _debug: ChannelValueProperty._debug("__init__") + Property.__init__(self, 'presentValue', ChannelValue, default=None, optional=False, mutable=True) + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: ChannelValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + + ### Clause 12.53.5, page 487 + raise NotImplementedError() + +class ChannelCmdObject(ChannelObject): + + properties = [ + ChannelValueProperty(), + ] diff --git a/py27/bacpypes/local/schedule.py b/py27/bacpypes/local/schedule.py index 91cbee62..4c8c97a1 100644 --- a/py27/bacpypes/local/schedule.py +++ b/py27/bacpypes/local/schedule.py @@ -4,7 +4,6 @@ Local Schedule Object """ -import sys import calendar from time import mktime as _mktime diff --git a/py27/bacpypes/netservice.py b/py27/bacpypes/netservice.py index bfb44a77..83fa1e0f 100755 --- a/py27/bacpypes/netservice.py +++ b/py27/bacpypes/netservice.py @@ -6,14 +6,17 @@ from copy import deepcopy as _deepcopy +from .settings import settings from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError +from .core import deferred from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement from .task import FunctionTask -from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation +from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation, \ + GlobalBroadcast from .npdu import NPDU, npdu_types, IAmRouterToNetwork, WhoIsRouterToNetwork, \ WhatIsNetworkNumber, NetworkNumberIs from .apdu import APDU as _APDU @@ -26,7 +29,7 @@ ROUTER_AVAILABLE = 0 # normal ROUTER_BUSY = 1 # router is busy ROUTER_DISCONNECTED = 2 # could make a connection, but hasn't -ROUTER_UNREACHABLE = 3 # cannot route +ROUTER_UNREACHABLE = 3 # temporarily unreachable # # RouterInfo @@ -36,13 +39,17 @@ class RouterInfo(DebugContents): """These objects are routing information records that map router addresses with destination networks.""" - _debug_contents = ('snet', 'address', 'dnets', 'status') + _debug_contents = ('snet', 'address', 'dnets') - def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + def __init__(self, snet, address): 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 + self.dnets = {} # {dnet: status} + + def set_status(self, dnets, status): + """Change the status of each of the DNETS.""" + for dnet in dnets: + self.dnets[dnet] = status # # RouterInfoCache @@ -54,109 +61,121 @@ 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) + self.routers = {} # snet -> {Address: RouterInfo} + self.path_info = {} # (snet, dnet) -> RouterInfo - # check to see if we know about it - if dnet not in self.networks: - if _debug: RouterInfoCache._debug(" - no route") - return None + def get_router_info(self, snet, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r %r", snet, dnet) # return the network and address - router_info = self.networks[dnet] + router_info = self.path_info.get((snet, dnet), None) 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) + return router_info - def update_router_info(self, snet, address, dnets): + def update_router_info(self, snet, address, dnets, status=ROUTER_AVAILABLE): 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] + existing_router_info = self.routers.get(snet, {}).get(address, None) - # add (or move) the destination networks + other_routers = set() 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") + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + if other_routers: + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + # update current router info if there is one + if not existing_router_info: + router_info = RouterInfo(snet, address) + if snet not in self.routers: + self.routers[snet] = {address: router_info} + else: + self.routers[snet][address] = router_info - # 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) + for dnet in dnets: + self.path_info[(snet, dnet)] = router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r via %r", snet, dnet, router_info.address) + router_info.dnets[dnet] = status + else: + for dnet in dnets: + if dnet not in existing_router_info.dnets: + self.path_info[(snet, dnet)] = existing_router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r via %r", snet, dnet, router_info.address) + existing_router_info.dnets[dnet] = status 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") + existing_router_info = self.routers.get(snet, {}).get(address, None) + if not existing_router_info: + if _debug: RouterInfoCache._debug(" - not a router we know about") return - router_info = self.routers[key] - router_info.status = status - if _debug: RouterInfoCache._debug(" - status updated") + existing_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 + if (address is None) and (dnets is None): + raise RuntimeError("inconsistent parameters") - # look up the router reference - key = (snet, address) - if key not in self.routers: - if _debug: RouterInfoCache._debug(" - unknown router") + # remove the dnets from a router or the whole router + if (address is not None): + router_info = self.routers.get(snet, {}).get(address, None) + if not router_info: + if _debug: RouterInfoCache._debug(" - no route info") + else: + for dnet in (dnets or router_info.dnets): + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + del self.routers[snet][address] return - router_info = self.routers[key] - if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + # look for routers to the dnets + other_routers = set() + for dnet in dnets: + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + def update_source_network(self, old_snet, new_snet): + if _debug: RouterInfoCache._debug("update_source_network %r %r", old_snet, new_snet) + + if old_snet not in self.routers: + if _debug: RouterInfoCache._debug(" - no router references: %r", list(self.routers.keys())) + return - # if dnets is None, remove all the networks for the router - if dnets is None: - dnets = router_info.dnets + # move the router info records to the new net + snet_routers = self.routers[new_snet] = self.routers.pop(old_snet) - # 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] + # update the paths + for address, router_info in snet_routers.items(): + for dnet in router_info.dnets: + self.path_info[(new_snet, dnet)] = self.path_info.pop((old_snet, dnet)) # # NetworkAdapter @@ -165,13 +184,19 @@ def delete_router_info(self, snet, address=None, dnets=None): @bacpypes_debugging class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') + _debug_contents = ( + 'adapterSAP-', + 'adapterNet', + 'adapterAddr', + 'adapterNetConfigured', + ) - def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) + def __init__(self, sap, net, addr, cid=None): + if _debug: NetworkAdapter._debug("__init__ %s %r %r cid=%r", sap, net, addr, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net + self.adapterAddr = addr # record if this was 0=learned, 1=configured, None=unknown if net is None: @@ -209,7 +234,7 @@ def DisconnectConnectionToNetwork(self, net): class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): _debug_contents = ('adapters++', 'pending_nets', - 'local_adapter-', 'local_address', + 'local_adapter-', ) def __init__(self, router_info_cache=None, sap=None, sid=None): @@ -226,36 +251,60 @@ def __init__(self, router_info_cache=None, sap=None, sid=None): # map to a list of application layer packets waiting for a path self.pending_nets = {} - # these are set when bind() is called + # 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.""" + """Create a network adapter object and bind. + + bind(s, None, None) + Called for simple applications, local network unknown, no specific + address, APDUs sent upstream + + bind(s, net, None) + Called for routers, bind to the network, (optionally?) drop APDUs + + bind(s, None, address) + Called for applications or routers, bind to the network (to be + discovered), send up APDUs with a metching address + + bind(s, net, address) + Called for applications or routers, bind to the network, send up + APDUs with a metching address. + """ if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) # make sure this hasn't already been called with this network if net in self.adapters: - raise RuntimeError("already bound") + raise RuntimeError("already bound: %r" % (net,)) # create an adapter object, add it to our map - adapter = NetworkAdapter(self, net) + adapter = NetworkAdapter(self, net, address) self.adapters[net] = adapter - if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r, %r", net, adapter) # if the address was given, make it the "local" one - if address and not self.local_address: + if address: + if _debug: NetworkServiceAccessPoint._debug(" - setting local adapter") self.local_adapter = adapter - self.local_address = address + + # if the local adapter isn't set yet, make it the first one, and can + # be overridden by a subsequent call if the address is specified + if not self.local_adapter: + if _debug: NetworkServiceAccessPoint._debug(" - default local adapter") + self.local_adapter = adapter + + if not self.local_adapter.adapterAddr: + if _debug: NetworkServiceAccessPoint._debug(" - no local address") # bind to the server bind(adapter, server) #----- - def add_router_references(self, snet, address, dnets): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) + def update_router_references(self, snet, address, dnets): + """Update references to routers.""" + if _debug: NetworkServiceAccessPoint._debug("update_router_references %r %r %r", snet, address, dnets) # see if we have an adapter for the snet if snet not in self.adapters: @@ -284,13 +333,9 @@ def indication(self, pdu): if (not self.adapters): raise ConfigurationError("no adapters") - # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.local_adapter): - raise ConfigurationError("local adapter must be set") - # get the local adapter - adapter = self.local_adapter or self.adapters[None] - if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) + local_adapter = self.local_adapter + if _debug: NetworkServiceAccessPoint._debug(" - local_adapter: %r", local_adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -305,14 +350,30 @@ def indication(self, pdu): # the hop count always starts out big npdu.npduHopCount = 255 + # if this is route aware, use it for the destination + if settings.route_aware and npdu.pduDestination.addrRoute: + # always a local station for now, in theory this could also be + # a local braodcast address, remote station, or remote broadcast + # but that is not supported by the patterns + assert npdu.pduDestination.addrRoute.addrType == Address.localStationAddr + if _debug: NetworkServiceAccessPoint._debug(" - routed: %r", npdu.pduDestination.addrRoute) + + if npdu.pduDestination.addrType in (Address.remoteStationAddr, Address.remoteBroadcastAddr, Address.globalBroadcastAddr): + if _debug: NetworkServiceAccessPoint._debug(" - continue DADR: %r", apdu.pduDestination) + npdu.npduDADR = apdu.pduDestination + + npdu.pduDestination = npdu.pduDestination.addrRoute + local_adapter.process_npdu(npdu) + return + # local stations given to local adapter if (npdu.pduDestination.addrType == Address.localStationAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # local broadcast given to local adapter if (npdu.pduDestination.addrType == Address.localBroadcastAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # global broadcast @@ -331,9 +392,10 @@ def indication(self, pdu): raise RuntimeError("invalid destination address type: %s" % (npdu.pduDestination.addrType,)) dnet = npdu.pduDestination.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - dnet: %r", dnet) # if the network matches the local adapter it's local - if (dnet == adapter.adapterNet): + if (dnet == local_adapter.adapterNet): if (npdu.pduDestination.addrType == Address.remoteStationAddr): if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) @@ -343,7 +405,7 @@ def indication(self, pdu): else: raise RuntimeError("addressing problem") - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # get it ready to send when the path is found @@ -356,49 +418,50 @@ def indication(self, pdu): self.pending_nets[dnet].append(npdu) return - # check cache for an available path - path_info = self.router_info_cache.get_router_info(dnet) + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break # 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) + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - router_info found: %r", router_info) - # 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) + ### check the path status + dnet_status = router_info.dnets[dnet] + if _debug: NetworkServiceAccessPoint._debug(" - dnet_status: %r", dnet_status) # fix the destination - npdu.pduDestination = address + npdu.pduDestination = router_info.address # send it along - adapter.process_npdu(npdu) - return + snet_adapter.process_npdu(npdu) - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") + else: + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # 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) + # 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() + # 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 adapter in self.adapters.values(): - ### make sure the adapter is OK - self.sap_indication(adapter, xnpdu) + # send it to all of the adapters + for adapter in self.adapters.values(): + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) # make sure our configuration is OK - if (not self.adapters): + if not self.adapters: raise ConfigurationError("no adapters") # check for source routing @@ -411,29 +474,14 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (1)") return - # 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) - - # see if the router has changed - if not (router_address == npdu.pduSource): - if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) - else: - if _debug: NetworkServiceAccessPoint._debug(" - new path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) + # 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): if _debug: NetworkServiceAccessPoint._debug(" - no DADR") - processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) + processLocally = (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -443,8 +491,7 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: @@ -454,9 +501,8 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_adapter.adapterAddr.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: @@ -489,29 +535,34 @@ def process_npdu(self, adapter, npdu): 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 ) + apdu.pduSource = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) else: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + apdu.pduSource.addrRoute = npdu.pduSource # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: - apdu.pduDestination = npdu.npduDADR + apdu.pduDestination = GlobalBroadcast() elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr else: # combine the source address if npdu.npduSADR: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + if _debug: NetworkServiceAccessPoint._debug(" - adding route") + apdu.pduSource.addrRoute = npdu.pduSource else: apdu.pduSource = npdu.pduSource # pass along global broadcast if npdu.npduDADR and npdu.npduDADR.addrType == Address.globalBroadcastAddr: - apdu.pduDestination = npdu.npduDADR + apdu.pduDestination = GlobalBroadcast() else: apdu.pduDestination = npdu.pduDestination if _debug: @@ -605,33 +656,27 @@ def process_npdu(self, adapter, npdu): xadapter.process_npdu(_deepcopy(newpdu)) return - # 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 + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break - xadapter = self.adapters[router_net] - if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) + # found a path + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", router_info) # the destination is the address of the router - newpdu.pduDestination = router_address + newpdu.pduDestination = router_info.address # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) + snet_adapter.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 xnpdu = WhoIsRouterToNetwork(dnet) xnpdu.pduDestination = LocalBroadcast() @@ -678,6 +723,8 @@ def sap_confirmation(self, adapter, npdu): @bacpypes_debugging class NetworkServiceElement(ApplicationServiceElement): + _startup_disabled = False + def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) @@ -685,6 +732,49 @@ def __init__(self, eid=None): # network number is timeout self.network_number_is_task = None + # if starting up is enabled defer our startup function + if not self._startup_disabled: + deferred(self.startup) + + def startup(self): + if _debug: NetworkServiceElement._debug("startup") + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # loop through all of the adapters + for adapter in sap.adapters.values(): + if _debug: NetworkServiceElement._debug(" - adapter: %r", adapter) + + if (adapter.adapterNet is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown net") + continue + elif (adapter.adapterAddr is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown addr") + continue + + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + if (xadapter.adapterNet is None) or (xadapter.adapterAddr is None): + continue + netlist.append(xadapter.adapterNet) + + # skip for an empty list, perhaps they are not yet learned + if not netlist: + if _debug: NetworkServiceElement._debug(" - skipping, no netlist") + continue + + # pass this along to the cache -- on hold #213 + # sap.router_info_cache.update_router_info(adapter.adapterNet, adapter.adapterAddr, netlist) + + # send an announcement + self.i_am_router_to_network(adapter=adapter, network=netlist) + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -765,10 +855,14 @@ def i_am_router_to_network(self, adapter=None, destination=None, network=None): netlist.append(xadapter.adapterNet) ### add the other reachable networks - if network is not None: + if network is None: + pass + elif isinstance(network, int): if network not in netlist: continue netlist = [network] + elif isinstance(network, list): + netlist = [net for net in netlist if net in network] # build a response iamrtn = IAmRouterToNetwork(netlist) @@ -808,7 +902,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # add the direct network netlist.append(xadapter.adapterNet) - ### add the other reachable + ### add the other reachable networks? if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -839,51 +933,51 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # send it back self.response(adapter, iamrtn) + return - else: - # see if there is routing information for this source network - router_info = sap.router_info_cache.get_router_info(dnet) + + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in sap.adapters.items(): + router_info = sap.router_info_cache.get_router_info(snet, 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 - if sap.adapters[router_net] is adapter: - if _debug: NetworkServiceElement._debug(" - same network") - return - - # build a response - iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource - - # send it back - self.response(adapter, iamrtn) + break - else: - if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + # found a path + if router_info: + if _debug: NetworkServiceElement._debug(" - router found: %r", router_info) - # build a request - whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) - whoisrtn.pduDestination = LocalBroadcast() + if snet_adapter is adapter: + if _debug: NetworkServiceElement._debug(" - same network") + return - # if the request had a source, forward it along - if npdu.npduSADR: - whoisrtn.npduSADR = npdu.npduSADR - else: - whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) - if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it to all of the (other) 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) + # send it back + self.response(adapter, iamrtn) + + else: + if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + + # build a request + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) + whoisrtn.pduDestination = LocalBroadcast() + + # if the request had a source, forward it along + if npdu.npduSADR: + whoisrtn.npduSADR = npdu.npduSADR + else: + whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) + if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + + # send it to all of the (other) 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) def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) @@ -893,7 +987,7 @@ def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - sap: %r", sap) # pass along to the service access point - sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + sap.update_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) # skip if this is not a router if len(sap.adapters) == 1: @@ -1091,6 +1185,9 @@ def NetworkNumberIs(self, adapter, npdu): if adapter.adapterNet is None: if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + # update the routing information + sap.router_info_cache.update_source_network(None, npdu.nniNet) + # delete the reference from an unknown network del sap.adapters[None] @@ -1101,7 +1198,6 @@ def NetworkNumberIs(self, adapter, npdu): sap.adapters[adapter.adapterNet] = adapter if _debug: NetworkServiceElement._debug(" - local network learned") - ###TODO: s/None/net/g in routing tables return # check if this matches what we have @@ -1116,6 +1212,9 @@ def NetworkNumberIs(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - learning something new") + # update the routing information + sap.router_info_cache.update_source_network(adapter.adapterNet, npdu.nniNet) + # delete the reference from the old (learned) network del sap.adapters[adapter.adapterNet] @@ -1125,5 +1224,3 @@ def NetworkNumberIs(self, adapter, npdu): # we now know what network this is sap.adapters[adapter.adapterNet] = adapter - ###TODO: s/old/new/g in routing tables - diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 2770068f..56361a7b 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -384,7 +384,7 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr @bacpypes_debugging class OptionalProperty(StandardProperty): - """The property is required to be present and readable using BACnet services.""" + """The property is optional and need not be present.""" def __init__(self, identifier, datatype, default=None, optional=True, mutable=False): if _debug: @@ -462,6 +462,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class Object(object): _debug_contents = ('_app',) + _object_supports_cov = False properties = \ [ ObjectIdentifierProperty('objectIdentifier', ObjectIdentifier, optional=False) @@ -469,6 +470,9 @@ class Object(object): , OptionalProperty('description', CharacterString) , OptionalProperty('profileName', CharacterString) , ReadableProperty('propertyList', ArrayOf(PropertyIdentifier)) + , OptionalProperty('tags', ArrayOf(NameValue)) + , OptionalProperty('profileLocation', CharacterString) + , OptionalProperty('profileName', CharacterString) ] _properties = {} @@ -732,6 +736,8 @@ class AccessCredentialObject(Object): @register_object_type class AccessDoorObject(Object): objectType = 'accessDoor' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', DoorValue) , ReadableProperty('statusFlags', StatusFlags) @@ -769,6 +775,8 @@ class AccessDoorObject(Object): @register_object_type class AccessPointObject(Object): objectType = 'accessPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('eventState', EventState) @@ -948,6 +956,8 @@ class AlertEnrollmentObject(Object): @register_object_type class AnalogInputObject(Object): objectType = 'analogInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -983,6 +993,8 @@ class AnalogInputObject(Object): @register_object_type class AnalogOutputObject(Object): objectType = 'analogOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -1019,6 +1031,8 @@ class AnalogOutputObject(Object): @register_object_type class AnalogValueObject(Object): objectType = 'analogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1071,6 +1085,8 @@ class AveragingObject(Object): @register_object_type class BinaryInputObject(Object): objectType = 'binaryInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1105,6 +1121,8 @@ class BinaryInputObject(Object): @register_object_type class BinaryOutputObject(Object): objectType = 'binaryOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1143,6 +1161,8 @@ class BinaryOutputObject(Object): @register_object_type class BinaryValueObject(Object): objectType = 'binaryValue' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , ReadableProperty('statusFlags',StatusFlags) @@ -1243,6 +1263,8 @@ class ChannelObject(Object): @register_object_type class CharacterStringValueObject(Object): objectType = 'characterstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -1282,6 +1304,8 @@ class CommandObject(Object): @register_object_type class CredentialDataInputObject(Object): objectType = 'credentialDataInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', AuthenticationFactor) , ReadableProperty('statusFlags', StatusFlags) @@ -1304,6 +1328,8 @@ class CredentialDataInputObject(Object): @register_object_type class DatePatternValueObject(Object): objectType = 'datePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1317,6 +1343,8 @@ class DatePatternValueObject(Object): @register_object_type class DateValueObject(Object): objectType = 'dateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1330,6 +1358,8 @@ class DateValueObject(Object): @register_object_type class DateTimePatternValueObject(Object): objectType = 'datetimePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1344,6 +1374,8 @@ class DateTimePatternValueObject(Object): @register_object_type class DateTimeValueObject(Object): objectType = 'datetimeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1543,6 +1575,8 @@ class GroupObject(Object): @register_object_type class IntegerValueObject(Object): objectType = 'integerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Integer) , ReadableProperty('statusFlags', StatusFlags) @@ -1578,6 +1612,8 @@ class IntegerValueObject(Object): @register_object_type class LargeAnalogValueObject(Object): objectType = 'largeAnalogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Double) , ReadableProperty('statusFlags', StatusFlags) @@ -1613,6 +1649,8 @@ class LargeAnalogValueObject(Object): @register_object_type class LifeSafetyPointObject(Object): objectType = 'lifeSafetyPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1651,6 +1689,8 @@ class LifeSafetyPointObject(Object): @register_object_type class LifeSafetyZoneObject(Object): objectType = 'lifeSafetyZone' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1687,6 +1727,8 @@ class LifeSafetyZoneObject(Object): @register_object_type class LightingOutputObject(Object): objectType = 'lightingOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , ReadableProperty('trackingValue', Real) @@ -1717,6 +1759,8 @@ class LightingOutputObject(Object): @register_object_type class LoadControlObject(Object): objectType = 'loadControl' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', ShedState) , OptionalProperty('stateDescription', CharacterString) @@ -1751,6 +1795,8 @@ class LoadControlObject(Object): @register_object_type class LoopObject(Object): objectType = 'loop' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1797,6 +1843,8 @@ class LoopObject(Object): @register_object_type class MultiStateInputObject(Object): objectType = 'multiStateInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1826,6 +1874,8 @@ class MultiStateInputObject(Object): @register_object_type class MultiStateOutputObject(Object): objectType = 'multiStateOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1856,6 +1906,8 @@ class MultiStateOutputObject(Object): @register_object_type class MultiStateValueObject(Object): objectType = 'multiStateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -1952,10 +2004,6 @@ class NetworkPortObject(Object): , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3)) #352 , OptionalProperty('eventState', EventState) #36 , ReadableProperty('reliabilityEvaluationInhibit', Boolean) #357 - , OptionalProperty('propertyList', ArrayOf(PropertyIdentifier)) #371 - , OptionalProperty('tags', ArrayOf(NameValue)) #486 - , OptionalProperty('profileLocation', CharacterString) #91 - , OptionalProperty('profileName', CharacterString) #168 ] @register_object_type @@ -2003,6 +2051,8 @@ class NotificationForwarderObject(Object): @register_object_type class OctetStringValueObject(Object): objectType = 'octetstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -2016,6 +2066,8 @@ class OctetStringValueObject(Object): @register_object_type class PositiveIntegerValueObject(Object): objectType = 'positiveIntegerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -2075,6 +2127,8 @@ class ProgramObject(Object): @register_object_type class PulseConverterObject(Object): objectType = 'pulseConverter' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('inputReference', ObjectPropertyReference) @@ -2149,6 +2203,8 @@ class StructuredViewObject(Object): @register_object_type class TimePatternValueObject(Object): objectType = 'timePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) @@ -2162,6 +2218,8 @@ class TimePatternValueObject(Object): @register_object_type class TimeValueObject(Object): objectType = 'timeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) diff --git a/py27/bacpypes/pdu.py b/py27/bacpypes/pdu.py index eb848bd6..85e64a5f 100755 --- a/py27/bacpypes/pdu.py +++ b/py27/bacpypes/pdu.py @@ -13,6 +13,7 @@ except ImportError: netifaces = None +from .settings import settings from .debugging import ModuleLogger, bacpypes_debugging, btox, xtob from .comm import PCI as _PCI, PDUData @@ -28,10 +29,27 @@ # Address # -ip_address_mask_port_re = re.compile(r'^(?:(\d+):)?(\d+\.\d+\.\d+\.\d+)(?:/(\d+))?(?::(\d+))?$') +_field_address = r"((?:\d+)|(?:0x(?:[0-9A-Fa-f][0-9A-Fa-f])+))" +_ip_address_port = r"(\d+\.\d+\.\d+\.\d+)(?::(\d+))?" +_ip_address_mask_port = r"(\d+\.\d+\.\d+\.\d+)(?:/(\d+))?(?::(\d+))?" +_net_ip_address_port = r"(\d+):" + _ip_address_port +_at_route = "(?:[@](?:" + _field_address + "|" + _ip_address_port + "))?" + +field_address_re = re.compile("^" + _field_address + "$") +ip_address_port_re = re.compile("^" + _ip_address_port + "$") +ip_address_mask_port_re = re.compile("^" + _ip_address_mask_port + "$") +net_ip_address_port_re = re.compile("^" + _net_ip_address_port + "$") +net_ip_address_mask_port_re = re.compile("^" + _net_ip_address_port + "$") + ethernet_re = re.compile(r'^([0-9A-Fa-f][0-9A-Fa-f][:]){5}([0-9A-Fa-f][0-9A-Fa-f])$' ) interface_re = re.compile(r'^(?:([\w]+))(?::(\d+))?$') +net_broadcast_route_re = re.compile("^([0-9])+:[*]" + _at_route + "$") +net_station_route_re = re.compile("^([0-9])+:" + _field_address + _at_route + "$") +net_ip_address_route_re = re.compile("^([0-9])+:" + _ip_address_port + _at_route + "$") + +combined_pattern = re.compile("^(?:(?:([0-9]+)|([*])):)?(?:([*])|" + _field_address + "|" + _ip_address_mask_port + ")" + _at_route + "$") + @bacpypes_debugging class Address: nullAddr = 0 @@ -45,8 +63,9 @@ def __init__(self, *args): if _debug: Address._debug("__init__ %r", args) self.addrType = Address.nullAddr self.addrNet = None - self.addrLen = 0 - self.addrAddr = b'' + self.addrAddr = None + self.addrLen = None + self.addrRoute = None if len(args) == 1: self.decode_address(args[0]) @@ -65,25 +84,22 @@ def decode_address(self, addr): """Initialize the address from a string. Lots of different forms are supported.""" if _debug: Address._debug("decode_address %r (%s)", addr, type(addr)) - # start out assuming this is a local station + # start out assuming this is a local station and didn't get routed self.addrType = Address.localStationAddr self.addrNet = None + self.addrAddr = None + self.addrLen = None + self.addrRoute = None if addr == "*": if _debug: Address._debug(" - localBroadcast") self.addrType = Address.localBroadcastAddr - self.addrNet = None - self.addrAddr = None - self.addrLen = None elif addr == "*:*": if _debug: Address._debug(" - globalBroadcast") self.addrType = Address.globalBroadcastAddr - self.addrNet = None - self.addrAddr = None - self.addrLen = None elif isinstance(addr, int): if _debug: Address._debug(" - int") @@ -96,44 +112,100 @@ def decode_address(self, addr): elif isinstance(addr, basestring): if _debug: Address._debug(" - str") - m = ip_address_mask_port_re.match(addr) + m = combined_pattern.match(addr) if m: - if _debug: Address._debug(" - IP address") + if _debug: Address._debug(" - combined pattern") + + (net, global_broadcast, + local_broadcast, + local_addr, + local_ip_addr, local_ip_net, local_ip_port, + route_addr, route_ip_addr, route_ip_port + ) = m.groups() + + if global_broadcast and local_broadcast: + if _debug: Address._debug(" - global broadcast") + self.addrType = Address.globalBroadcastAddr + + elif net and local_broadcast: + if _debug: Address._debug(" - remote broadcast") + net_addr = int(net) + if (net_addr >= 65535): + raise ValueError("network out of range") + self.addrType = Address.remoteBroadcastAddr + self.addrNet = net_addr - net, addr, mask, port = m.groups() - if not mask: mask = '32' - if not port: port = '47808' - if _debug: Address._debug(" - net, addr, mask, port: %r, %r, %r, %r", net, addr, mask, port) + elif local_broadcast: + if _debug: Address._debug(" - local broadcast") + self.addrType = Address.localBroadcastAddr - if net: - net = int(net) - if (net >= 65535): + elif net: + if _debug: Address._debug(" - remote station") + net_addr = int(net) + if (net_addr >= 65535): raise ValueError("network out of range") self.addrType = Address.remoteStationAddr - self.addrNet = net - - self.addrPort = int(port) - self.addrTuple = (addr, self.addrPort) - - addrstr = socket.inet_aton(addr) - self.addrIP = struct.unpack('!L', addrstr)[0] - self.addrMask = (_long_mask << (32 - int(mask))) & _long_mask - self.addrHost = (self.addrIP & ~self.addrMask) - self.addrSubnet = (self.addrIP & self.addrMask) - - bcast = (self.addrSubnet | ~self.addrMask) - self.addrBroadcastTuple = (socket.inet_ntoa(struct.pack('!L', bcast & _long_mask)), self.addrPort) - - self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) - self.addrLen = 6 - - elif ethernet_re.match(addr): + self.addrNet = net_addr + + if local_addr: + if _debug: Address._debug(" - simple address") + if local_addr.startswith("0x"): + self.addrAddr = xtob(local_addr[2:]) + self.addrLen = len(self.addrAddr) + else: + local_addr = int(local_addr) + if local_addr >= 256: + raise ValueError("address out of range") + + self.addrAddr = struct.pack('B', local_addr) + self.addrLen = 1 + + if local_ip_addr: + if _debug: Address._debug(" - ip address") + if not local_ip_port: + local_ip_port = '47808' + if not local_ip_net: + local_ip_net = '32' + + self.addrPort = int(local_ip_port) + self.addrTuple = (local_ip_addr, self.addrPort) + if _debug: Address._debug(" - addrTuple: %r", self.addrTuple) + + addrstr = socket.inet_aton(local_ip_addr) + self.addrIP = struct.unpack('!L', addrstr)[0] + self.addrMask = (_long_mask << (32 - int(local_ip_net))) & _long_mask + self.addrHost = (self.addrIP & ~self.addrMask) + self.addrSubnet = (self.addrIP & self.addrMask) + + bcast = (self.addrSubnet | ~self.addrMask) + self.addrBroadcastTuple = (socket.inet_ntoa(struct.pack('!L', bcast & _long_mask)), self.addrPort) + if _debug: Address._debug(" - addrBroadcastTuple: %r", self.addrBroadcastTuple) + + self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) + self.addrLen = 6 + + if (not settings.route_aware) and (route_addr or route_ip_addr): + Address._warning("route provided but not route aware: %r", addr) + + if route_addr: + self.addrRoute = Address(int(route_addr)) + if _debug: Address._debug(" - addrRoute: %r", self.addrRoute) + elif route_ip_addr: + if not route_ip_port: + route_ip_port = '47808' + self.addrRoute = Address((route_ip_addr, int(route_ip_port))) + if _debug: Address._debug(" - addrRoute: %r", self.addrRoute) + + return + + if ethernet_re.match(addr): if _debug: Address._debug(" - ethernet") self.addrAddr = xtob(addr, ':') self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+$", addr): + if re.match(r"^\d+$", addr): if _debug: Address._debug(" - int") addr = int(addr) @@ -142,8 +214,9 @@ def decode_address(self, addr): self.addrAddr = struct.pack('B', addr) self.addrLen = 1 + return - elif re.match(r"^\d+:[*]$", addr): + if re.match(r"^\d+:[*]$", addr): if _debug: Address._debug(" - remote broadcast") addr = int(addr[:-2]) @@ -154,8 +227,9 @@ def decode_address(self, addr): self.addrNet = addr self.addrAddr = None self.addrLen = None + return - elif re.match(r"^\d+:\d+$",addr): + if re.match(r"^\d+:\d+$",addr): if _debug: Address._debug(" - remote station") net, addr = addr.split(':') @@ -170,20 +244,23 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = struct.pack('B', addr) self.addrLen = 1 + return - elif re.match(r"^0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): + if re.match(r"^0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): if _debug: Address._debug(" - modern hex string") self.addrAddr = xtob(addr[2:]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): + if re.match(r"^X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): if _debug: Address._debug(" - old school hex string") self.addrAddr = xtob(addr[2:-1]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+:0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): + if re.match(r"^\d+:0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): if _debug: Address._debug(" - remote station with modern hex string") net, addr = addr.split(':') @@ -195,8 +272,9 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = xtob(addr[2:]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+:X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): + if re.match(r"^\d+:X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): if _debug: Address._debug(" - remote station with old school hex string") net, addr = addr.split(':') @@ -208,8 +286,11 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = xtob(addr[2:-1]) self.addrLen = len(self.addrAddr) + return + + if netifaces and interface_re.match(addr): + if _debug: Address._debug(" - interface name with optional port") - elif netifaces and interface_re.match(addr): interface, port = interface_re.match(addr).groups() if port is not None: self.addrPort = int(port) @@ -219,6 +300,7 @@ def decode_address(self, addr): interfaces = netifaces.interfaces() if interface not in interfaces: raise ValueError("not an interface: %s" % (interface,)) + if _debug: Address._debug(" - interfaces: %r", interfaces) ifaddresses = netifaces.ifaddresses(interface) if netifaces.AF_INET not in ifaddresses: @@ -228,9 +310,11 @@ def decode_address(self, addr): if len(ipv4addresses) > 1: raise ValueError("interface supports multiple IPv4 addresses: %s" % (interface,)) ifaddress = ipv4addresses[0] + if _debug: Address._debug(" - ifaddress: %r", ifaddress) addr = ifaddress['addr'] self.addrTuple = (addr, self.addrPort) + if _debug: Address._debug(" - addrTuple: %r", self.addrTuple) addrstr = socket.inet_aton(addr) self.addrIP = struct.unpack('!L', addrstr)[0] @@ -248,12 +332,13 @@ def decode_address(self, addr): self.addrBroadcastTuple = (ifaddress['broadcast'], self.addrPort) else: self.addrBroadcastTuple = None + if _debug: Address._debug(" - addrBroadcastTuple: %r", self.addrBroadcastTuple) self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) self.addrLen = 6 + return - else: - raise ValueError("unrecognized format") + raise ValueError("unrecognized format") elif isinstance(addr, tuple): addr, port = addr @@ -285,16 +370,15 @@ def decode_address(self, addr): self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) self.addrLen = 6 - else: raise TypeError("integer, string or tuple required") def __str__(self): if self.addrType == Address.nullAddr: - return 'Null' + rslt = 'Null' elif self.addrType == Address.localBroadcastAddr: - return '*' + rslt = '*' elif self.addrType == Address.localStationAddr: rslt = '' @@ -308,10 +392,9 @@ def __str__(self): rslt += ':' + str(port) else: rslt += '0x' + btox(self.addrAddr) - return rslt elif self.addrType == Address.remoteBroadcastAddr: - return '%d:*' % (self.addrNet,) + rslt = '%d:*' % (self.addrNet,) elif self.addrType == Address.remoteStationAddr: rslt = '%d:' % (self.addrNet,) @@ -325,31 +408,52 @@ def __str__(self): rslt += ':' + str(port) else: rslt += '0x' + btox(self.addrAddr) - return rslt elif self.addrType == Address.globalBroadcastAddr: - return '*:*' + rslt = "*:*" else: raise TypeError("unknown address type %d" % self.addrType) + if self.addrRoute: + rslt += "@" + str(self.addrRoute) + + return rslt + def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.__str__()) + def _tuple(self): + if (not settings.route_aware) or (self.addrRoute is None): + return (self.addrType, self.addrNet, self.addrAddr, None) + else: + return (self.addrType, self.addrNet, self.addrAddr, self.addrRoute._tuple()) + def __hash__(self): - return hash( (self.addrType, self.addrNet, self.addrAddr) ) + return hash(self._tuple()) - def __eq__(self,arg): + def __eq__(self, arg): # try an coerce it into an address if not isinstance(arg, Address): arg = Address(arg) - # all of the components must match - return (self.addrType == arg.addrType) and (self.addrNet == arg.addrNet) and (self.addrAddr == arg.addrAddr) + # basic components must match + rslt = (self.addrType == arg.addrType) + rslt = rslt and (self.addrNet == arg.addrNet) + rslt = rslt and (self.addrAddr == arg.addrAddr) - def __ne__(self,arg): + # if both have routes they must match + if rslt and self.addrRoute and arg.addrRoute: + rslt = rslt and (self.addrRoute == arg.addrRoute) + + return rslt + + def __ne__(self, arg): return not self.__eq__(arg) + def __lt__(self, arg): + return self._tuple() < arg._tuple() + def dict_contents(self, use_dict=None, as_class=None): """Return the contents of an object as a dict.""" if _debug: _log.debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) @@ -377,9 +481,10 @@ def unpack_ip_addr(addr): class LocalStation(Address): - def __init__(self, addr): + def __init__(self, addr, route=None): self.addrType = Address.localStationAddr self.addrNet = None + self.addrRoute = route if isinstance(addr, int): if (addr < 0) or (addr >= 256): @@ -403,7 +508,7 @@ def __init__(self, addr): class RemoteStation(Address): - def __init__(self, net, addr): + def __init__(self, net, addr, route=None): if not isinstance(net, int): raise TypeError("integer network required") if (net < 0) or (net >= 65535): @@ -411,6 +516,7 @@ def __init__(self, net, addr): self.addrType = Address.remoteStationAddr self.addrNet = net + self.addrRoute = route if isinstance(addr, int): if (addr < 0) or (addr >= 256): @@ -434,11 +540,12 @@ def __init__(self, net, addr): class LocalBroadcast(Address): - def __init__(self): + def __init__(self, route=None): self.addrType = Address.localBroadcastAddr self.addrNet = None self.addrAddr = None self.addrLen = None + self.addrRoute = route # # RemoteBroadcast @@ -446,7 +553,7 @@ def __init__(self): class RemoteBroadcast(Address): - def __init__(self, net): + def __init__(self, net, route=None): if not isinstance(net, int): raise TypeError("integer network required") if (net < 0) or (net >= 65535): @@ -456,6 +563,7 @@ def __init__(self, net): self.addrNet = net self.addrAddr = None self.addrLen = None + self.addrRoute = route # # GlobalBroadcast @@ -463,11 +571,12 @@ def __init__(self, net): class GlobalBroadcast(Address): - def __init__(self): + def __init__(self, route=None): self.addrType = Address.globalBroadcastAddr self.addrNet = None self.addrAddr = None self.addrLen = None + self.addrRoute = route # # PCI diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 43c09e9a..41ce4e81 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -8,6 +8,7 @@ import struct import time import re +import unicodedata from .debugging import ModuleLogger, btox @@ -616,6 +617,14 @@ def __init__(self, arg=None): if not self.is_valid(arg): raise ValueError("value out of range") self.value = arg + elif isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + raise TypeError("invalid constructor datatype") + if not self.is_valid(arg): + raise ValueError("value out of range") + self.value = arg elif isinstance(arg, Unsigned): if not self.is_valid(arg.value): raise ValueError("value out of range") @@ -651,7 +660,12 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - if not isinstance(arg, (int, long)) or isinstance(arg, bool): + if isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + return False + elif not isinstance(arg, (int, long)) or isinstance(arg, bool): return False if (arg < cls._low_limit): return False @@ -920,8 +934,18 @@ def decode(self, tag): # normalize the value if (self.strEncoding == 0): - udata = self.strValue.decode('utf_8') - self.value = str(udata.encode('ascii', 'backslashreplace')) + try: + udata = self.strValue.decode('utf_8') + self.value = str(udata.encode('ascii', 'backslashreplace')) + except UnicodeDecodeError: + # Wrong encoding... trying with latin-1 as + # we probably face a Windows software encoding issue + try: + udata = self.strValue.decode('latin_1') + norm = unicodedata.normalize('NFKD', udata) + self.value = str(norm.encode('ascii', 'ignore')) + except UnicodeDecodeError: + raise elif (self.strEncoding == 3): udata = self.strValue.decode('utf_32be') self.value = str(udata.encode('ascii', 'backslashreplace')) @@ -1658,6 +1682,7 @@ class ObjectType(Enumerated): , 'timeValue':50 , 'trendLog':20 , 'trendLogMultiple':27 + , 'networkPort':56 } expand_enumerations(ObjectType) diff --git a/py27/bacpypes/service/cov.py b/py27/bacpypes/service/cov.py index 21fd0b60..3ba68158 100644 --- a/py27/bacpypes/service/cov.py +++ b/py27/bacpypes/service/cov.py @@ -471,6 +471,7 @@ def send_cov_notifications(self, subscription=None): # mapping from object type to appropriate criteria class criteria_type_map = { +# 'accessDoor': GenericCriteria, #TODO: needs AccessDoorCriteria 'accessPoint': AccessPointCriteria, 'analogInput': COVIncrementCriteria, 'analogOutput': COVIncrementCriteria, @@ -497,6 +498,7 @@ def send_cov_notifications(self, subscription=None): 'dateTimePatternValue': GenericCriteria, 'credentialDataInput': CredentialDataInputCriteria, 'loadControl': LoadControlCriteria, + 'loop': GenericCriteria, 'pulseConverter': PulseConverterCriteria, } @@ -692,6 +694,10 @@ def do_SubscribeCOVRequest(self, apdu): if not obj: raise ExecutionError(errorClass='object', errorCode='unknownObject') + # check to see if the object supports COV + if not obj._object_supports_cov: + raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed') + # look for an algorithm already associated with this object cov_detection = self.cov_detections.get(obj, None) diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 11be52b0..f1095109 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -25,6 +25,12 @@ def __init__(self): if _debug: WhoIsIAmServices._debug("__init__") Capability.__init__(self) + def startup(self): + if _debug: WhoIsIAmServices._debug("startup") + + # send a global broadcast I-Am + self.i_am() + def who_is(self, low_limit=None, high_limit=None, address=None): if _debug: WhoIsIAmServices._debug("who_is") diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index 5c4c993f..c74e7df1 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -5,7 +5,7 @@ from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array, ArrayOf +from ..constructeddata import Any, Array, ArrayOf, List from ..apdu import \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ @@ -72,6 +72,8 @@ def do_ReadPropertyRequest(self, apdu): elif not isinstance(value, datatype.subtype): raise TypeError("invalid result datatype, expecting {0} and got {1}" \ .format(datatype.subtype.__name__, type(value).__name__)) + elif issubclass(datatype, List): + value = datatype(value) elif not isinstance(value, datatype): raise TypeError("invalid result datatype, expecting {0} and got {1}" \ .format(datatype.__name__, type(value).__name__)) @@ -288,6 +290,11 @@ def do_ReadPropertyMultipleRequest(self, apdu): else: for propId, prop in obj._properties.items(): if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + + # skip propertyList for ReadPropertyMultiple + if (propId == 'propertyList'): + if _debug: ReadWritePropertyMultipleServices._debug(" - ignore propertyList") + continue if (propertyIdentifier == 'all'): pass diff --git a/py27/bacpypes/settings.py b/py27/bacpypes/settings.py new file mode 100644 index 00000000..ba89b099 --- /dev/null +++ b/py27/bacpypes/settings.py @@ -0,0 +1,95 @@ +#!/usr/bin/python + +""" +Settings +""" + +import os + + +class Settings(dict): + """ + Settings + """ + + def __getattr__(self, name): + if name not in self: + raise AttributeError("No such setting: " + name) + return self[name] + + def __setattr__(self, name, value): + if name not in self: + raise AttributeError("No such setting: " + name) + self[name] = value + + +# globals +settings = Settings( + debug=set(), + color=False, + debug_file="", + max_bytes=1048576, + backup_count=5, + route_aware=False, +) + + +def os_settings(): + """ + Update the settings from known OS environment variables. + """ + for setting_name, env_name in ( + ("debug", "BACPYPES_DEBUG"), + ("color", "BACPYPES_COLOR"), + ("debug_file", "BACPYPES_DEBUG_FILE"), + ("max_bytes", "BACPYPES_MAX_BYTES"), + ("backup_count", "BACPYPES_BACKUP_COUNT"), + ("route_aware", "BACPYPES_ROUTE_AWARE"), + ): + env_value = os.getenv(env_name, None) + if env_value is not None: + cur_value = settings[setting_name] + + if isinstance(cur_value, bool): + env_value = env_value.lower() + if env_value in ("set", "true"): + env_value = True + elif env_value in ("reset", "false"): + env_value = False + else: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, int): + try: + env_value = int(env_value) + except: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, str): + pass + elif isinstance(cur_value, list): + env_value = env_value.split() + elif isinstance(cur_value, set): + env_value = set(env_value.split()) + else: + raise TypeError("setting type: " + setting_name) + settings[setting_name] = env_value + + +def dict_settings(**kwargs): + """ + Update the settings from key/value content. Lists are morphed into sets + if necessary, giving a setting any value is acceptable if there isn't one + already set, otherwise protect against setting type changes. + """ + for setting_name, kw_value in kwargs.items(): + cur_value = settings.get(setting_name, None) + + if cur_value is None: + pass + elif isinstance(cur_value, set): + if isinstance(kw_value, list): + kw_value = set(kw_value) + elif not isinstance(kw_value, set): + raise TypeError(setting_name) + elif not isinstance(kw_value, type(cur_value)): + raise TypeError("setting type: " + setting_name) + settings[setting_name] = kw_value diff --git a/py27/bacpypes/udp.py b/py27/bacpypes/udp.py index 589c5fdd..05efe0ba 100755 --- a/py27/bacpypes/udp.py +++ b/py27/bacpypes/udp.py @@ -151,7 +151,12 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non self.set_reuse_addr() # proceed with the bind - self.bind(address) + try: + self.bind(address) + except socket.error as err: + if _debug: UDPDirector._debug(" - bind error: %r", err) + self.close() + raise if _debug: UDPDirector._debug(" - getsockname: %r", self.socket.getsockname()) # allow it to send broadcasts diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 18a2c1a0..f57571ff 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,10 +18,12 @@ # Project Metadata # -__version__ = '0.17.6' +__version__ = '0.17.7' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' +from . import settings + # # Communications Core Modules # diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index df35fd9f..3650a8b6 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -23,7 +23,8 @@ except: pass -from .debugging import ModuleLogger, bacpypes_debugging, btox, xtob +from .settings import settings +from .debugging import ModuleLogger, bacpypes_debugging, btox from .pdu import PDU, Address from .bvll import BVLPDU, bvl_pdu_types, ForwardedNPDU, \ @@ -216,7 +217,10 @@ def decode_packet(data): # lift the address for forwarded NPDU's if atype is ForwardedNPDU: + old_pdu_source = pdu.pduSource pdu.pduSource = bpdu.bvlciAddress + if settings.route_aware: + pdu.pduSource.addrRoute = old_pdu_source # no deeper decoding for some elif atype not in (DistributeBroadcastToNetwork, OriginalUnicastNPDU, OriginalBroadcastNPDU): return pdu @@ -254,6 +258,8 @@ def decode_packet(data): # "lift" the source and destination address if npdu.npduSADR: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + apdu.pduSource.addrRoute = npdu.pduSource else: apdu.pduSource = npdu.pduSource if npdu.npduDADR: diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index acf2c1a2..c6542100 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -1574,11 +1574,12 @@ class ReinitializeDeviceRequestReinitializedStateOfDevice(Enumerated): enumerations = \ { 'coldstart':0 , 'warmstart':1 - , 'startbackup':2 - , 'endbackup':3 - , 'startrestore':4 - , 'endrestore':5 - , 'abortrestore':6 + , 'startBackup':2 + , 'endBackup':3 + , 'startRestore':4 + , 'endRestore':5 + , 'abortRestore':6 + , 'activateChanges':7 } class ReinitializeDeviceRequest(ConfirmedRequestSequence): diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index b7b54f56..b3bc96ee 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -7,6 +7,8 @@ import warnings from .debugging import bacpypes_debugging, DebugContents, ModuleLogger + +from .core import deferred from .comm import ApplicationServiceElement, bind from .iocb import IOController, SieveQueue @@ -209,6 +211,8 @@ def release(self, device_info): @bacpypes_debugging class Application(ApplicationServiceElement, Collector): + _startup_disabled = False + def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, aseID=None): if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) ApplicationServiceElement.__init__(self, aseID) @@ -250,6 +254,12 @@ def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, as # now set up the rest of the capabilities Collector.__init__(self) + # if starting up is enabled, find all the startup functions + if not self._startup_disabled: + for fn in self.capability_functions('startup'): + if _debug: Application._debug(" - startup fn: %r" , fn) + deferred(fn, self) + def add_object(self, obj): """Add an object to the local collection.""" if _debug: Application._debug("add_object %r", obj) @@ -517,7 +527,7 @@ def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): bind(self.bip, self.annexj, self.mux.annexJ) # bind the BIP stack to the network, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) def close_socket(self): if _debug: BIPSimpleApplication._debug("close_socket") @@ -578,7 +588,7 @@ def __init__(self, localDevice, localAddress, bbmdAddress, bbmdTTL, deviceInfoCa bind(self.bip, self.annexj, self.mux.annexJ) # bind the NSAP to the stack, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) def close_socket(self): if _debug: BIPForeignApplication._debug("close_socket") @@ -616,10 +626,11 @@ def __init__(self, localAddress, bbmdAddress=None, bbmdTTL=None, eID=None): else: self.bip = BIPForeign(bbmdAddress, bbmdTTL) self.annexj = AnnexJCodec() - self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True) + self.mux = UDPMultiplexer(self.localAddress, noBroadcast=False) # bind the bottom layers bind(self.bip, self.annexj, self.mux.annexJ) # bind the NSAP to the stack, no network number - self.nsap.bind(self.bip) + self.nsap.bind(self.bip, address=self.localAddress) + diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index e4390b1f..6504e3e5 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -516,7 +516,6 @@ def segmented_request(self, apdu): if _debug: ClientSSM._debug(" - error/reject/abort") self.set_state(COMPLETED) - self.response = apdu self.response(apdu) else: diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index 6e5b75e5..1bc45450 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -4,11 +4,12 @@ Base Types """ -from .debugging import ModuleLogger +from .debugging import bacpypes_debugging, ModuleLogger +from .errors import MissingRequiredParameter -from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ +from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned, Unsigned16 + Unsigned, Unsigned16, Tag from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -1716,12 +1717,88 @@ class RouterEntry(Sequence): , Element('status', RouterEntryStatus) # Defined Above ] +@bacpypes_debugging class NameValue(Sequence): sequenceElements = \ [ Element('name', CharacterString) - , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + , Element('value', AnyAtomic, None, True) ] + def __init__(self, name=None, value=None): + if _debug: NameValue._debug("__init__ name=%r value=%r", name, value) + + # default to no value + self.name = name + self.value = None + + if value is None: + pass + elif isinstance(value, (Atomic, DateTime)): + self.value = value + elif isinstance(value, Tag): + self.value = value.app_to_object() + else: + raise TypeError("invalid constructor datatype") + + def encode(self, taglist): + if _debug: NameValue._debug("(%r)encode %r", self.__class__.__name__, taglist) + + # build a tag and encode the name into it + tag = Tag() + CharacterString(self.name).encode(tag) + taglist.append(tag.app_to_context(0)) + + # the value is optional + if self.value is not None: + if isinstance(self.value, DateTime): + # has its own encoder + self.value.encode(taglist) + else: + # atomic values encode into a tag + tag = Tag() + self.value.encode(tag) + taglist.append(tag) + + def decode(self, taglist): + if _debug: NameValue._debug("(%r)decode %r", self.__class__.__name__, taglist) + + # no contents yet + self.name = None + self.value = None + + # look for the context encoded character string + tag = taglist.Peek() + if _debug: NameValue._debug(" - name tag: %r", tag) + if (tag is None) or (tag.tagClass != Tag.contextTagClass) or (tag.tagNumber != 0): + raise MissingRequiredParameter("%s is a missing required element of %s" % ('name', self.__class__.__name__)) + + # pop it off and save the value + taglist.Pop() + tag = tag.context_to_app(Tag.characterStringAppTag) + self.name = CharacterString(tag).value + + # look for the optional application encoded value + tag = taglist.Peek() + if _debug: NameValue._debug(" - value tag: %r", tag) + if tag and (tag.tagClass == Tag.applicationTagClass): + + # if it is a date check the next one for a time + if (tag.tagNumber == Tag.dateAppTag) and (len(taglist.tagList) >= 2): + next_tag = taglist.tagList[1] + if _debug: NameValue._debug(" - next_tag: %r", next_tag) + + if (next_tag.tagClass == Tag.applicationTagClass) and (next_tag.tagNumber == Tag.timeAppTag): + if _debug: NameValue._debug(" - remaining tag list 0: %r", taglist.tagList) + + self.value = DateTime() + self.value.decode(taglist) + if _debug: NameValue._debug(" - date time value: %r", self.value) + + # just a primitive value + if self.value is None: + taglist.Pop() + self.value = tag.app_to_object() + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index 42a08569..5c334f92 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -5,9 +5,8 @@ """ import sys -import struct -from time import time as _time +from .settings import settings from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .udp import UDPDirector @@ -90,6 +89,7 @@ def __init__(self, addr=None, noBroadcast=False): UDPMultiplexer._debug(" - address: %r", self.address) UDPMultiplexer._debug(" - addrTuple: %r", self.addrTuple) UDPMultiplexer._debug(" - addrBroadcastTuple: %r", self.addrBroadcastTuple) + UDPMultiplexer._debug(" - route_aware: %r", settings.route_aware) # create and bind the direct address self.direct = _MultiplexClient(self) @@ -100,7 +100,7 @@ def __init__(self, addr=None, noBroadcast=False): if specialBroadcast and (not noBroadcast) and sys.platform in ('linux', 'darwin'): self.broadcast = _MultiplexClient(self) self.broadcastPort = UDPDirector(self.addrBroadcastTuple, reuse=True) - bind(self.direct, self.broadcastPort) + bind(self.broadcast, self.broadcastPort) else: self.broadcast = None self.broadcastPort = None @@ -120,7 +120,7 @@ def close_socket(self): def indication(self, server, pdu): if _debug: UDPMultiplexer._debug("indication %r %r", server, pdu) - # check for a broadcast message + # broadcast message if pdu.pduDestination.addrType == Address.localBroadcastAddr: dest = self.addrBroadcastTuple if _debug: UDPMultiplexer._debug(" - requesting local broadcast: %r", dest) @@ -129,6 +129,7 @@ def indication(self, server, pdu): if not dest: return + # unicast message elif pdu.pduDestination.addrType == Address.localStationAddr: dest = unpack_ip_addr(pdu.pduDestination.addrAddr) if _debug: UDPMultiplexer._debug(" - requesting local station: %r", dest) @@ -140,6 +141,7 @@ def indication(self, server, pdu): def confirmation(self, client, pdu): if _debug: UDPMultiplexer._debug("confirmation %r %r", client, pdu) + if _debug: UDPMultiplexer._debug(" - client address: %r", client.multiplexer.address) # if this came from ourselves, dump it if pdu.pduSource == self.addrTuple: @@ -151,11 +153,14 @@ def confirmation(self, client, pdu): # match the destination in case the stack needs it if client is self.direct: + if _debug: UDPMultiplexer._debug(" - direct to us") dest = self.address elif client is self.broadcast: + if _debug: UDPMultiplexer._debug(" - broadcast to us") dest = LocalBroadcast() else: raise RuntimeError("confirmation mismatch") + if _debug: UDPMultiplexer._debug(" - dest: %r", dest) # must have at least one octet if not pdu.pduData: @@ -208,6 +213,11 @@ def indication(self, pdu): # check for broadcasts elif pdu.pduDestination.addrType == Address.localBroadcastAddr: +# if route_aware and pdu.pduDestination.addrRoute: +# xpdu = PDU(pdu.pduData, destination=pdu.pduDestination.addrRoute) +# self.request(xpdu) +# return + # loop through the peers for peerAddr in self.peers.keys(): xpdu = PDU(pdu.pduData, destination=peerAddr) @@ -346,6 +356,8 @@ def indication(self, pdu): if pdu.pduDestination.addrType == Address.localStationAddr: # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute if _debug: BIPSimple._debug(" - xpdu: %r", xpdu) # send it downstream @@ -355,6 +367,8 @@ def indication(self, pdu): elif pdu.pduDestination.addrType == Address.localBroadcastAddr: # make an original broadcast PDU xpdu = OriginalBroadcastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute if _debug: BIPSimple._debug(" - xpdu: %r", xpdu) # send it downstream @@ -398,6 +412,9 @@ def confirmation(self, pdu): elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) +# if route_aware: +# xpdu.pduSource.addrRoute = pdu.pduSource + if _debug: BIPSimple._debug(" - xpdu: %r", xpdu) # send it upstream @@ -492,8 +509,10 @@ def indication(self, pdu): # check for local stations if pdu.pduDestination.addrType == Address.localStationAddr: # make an original unicast PDU - xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) - xpdu.pduDestination = pdu.pduDestination + xpdu = OriginalUnicastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute + if _debug: BIPForeign._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -505,9 +524,11 @@ def indication(self, pdu): if _debug: BIPForeign._debug(" - packet dropped, unregistered") return - # make an original broadcast PDU - xpdu = DistributeBroadcastToNetwork(pdu, user_data=pdu.pduUserData) - xpdu.pduDestination = self.bbmdAddress + # make a broadcast PDU + xpdu = DistributeBroadcastToNetwork(pdu, destination=self.bbmdAddress, user_data=pdu.pduUserData) +# if route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute + if _debug: BIPForeign._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -561,6 +582,8 @@ def confirmation(self, pdu): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) +# if route_aware: +# xpdu.pduSource.pduRoute = pdu.pduSource # send it upstream self.response(xpdu) @@ -696,8 +719,9 @@ def indication(self, pdu): # check for local stations if pdu.pduDestination.addrType == Address.localStationAddr: # make an original unicast PDU - xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) - xpdu.pduDestination = pdu.pduDestination + xpdu = OriginalUnicastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if settings.route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream @@ -706,13 +730,18 @@ def indication(self, pdu): # check for broadcasts elif pdu.pduDestination.addrType == Address.localBroadcastAddr: # make an original broadcast PDU - xpdu = OriginalBroadcastNPDU(pdu, user_data=pdu.pduUserData) - xpdu.pduDestination = pdu.pduDestination + xpdu = OriginalBroadcastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if settings.route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute if _debug: BIPBBMD._debug(" - original broadcast xpdu: %r", xpdu) # send it downstream self.request(xpdu) + # skip other processing if the route was provided +# if settings.route_aware and pdu.pduDestination.addrRoute: +# return + # make a forwarded PDU xpdu = ForwardedNPDU(self.bbmdAddress, pdu, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) @@ -768,6 +797,8 @@ def confirmation(self, pdu): if self.serverPeer: # build a PDU with a local broadcast address xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) +# if settings.route_aware: +# xpdu.pduSource.addrRoute = pdu.pduSource if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) self.response(xpdu) @@ -1031,8 +1062,9 @@ def indication(self, pdu): ###TODO the destination should be a peer or a registered foreign device # make an original unicast PDU - xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) - xpdu.pduDestination = pdu.pduDestination + xpdu = OriginalUnicastNPDU(pdu, destination=pdu.pduDestination, user_data=pdu.pduUserData) +# if settings.route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute if _debug: BIPNAT._debug(" - xpdu: %r", xpdu) # send it downstream @@ -1044,11 +1076,17 @@ def indication(self, pdu): xpdu = ForwardedNPDU(self.bbmdAddress, pdu, user_data=pdu.pduUserData) if _debug: BIPNAT._debug(" - forwarded xpdu: %r", xpdu) +# if settings.route_aware and pdu.pduDestination.addrRoute: +# xpdu.pduDestination = pdu.pduDestination.addrRoute +# if _debug: BIPNAT._debug(" - sending to specific route: %r", xpdu.pduDestination) +# self.request(xpdu) +# return + # send it to the peers, all of them have all F's mask for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address((bdte.addrIP, bdte.addrPort)) - BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPNAT._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices @@ -1098,6 +1136,8 @@ def confirmation(self, pdu): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) +# if settings.route_aware: +# xpdu.pduSource.addrRoute = pdu.pduSource if _debug: BIPNAT._debug(" - upstream xpdu: %r", xpdu) # send it upstream diff --git a/py34/bacpypes/consolelogging.py b/py34/bacpypes/consolelogging.py index 4deae567..1285bfd6 100755 --- a/py34/bacpypes/consolelogging.py +++ b/py34/bacpypes/consolelogging.py @@ -6,10 +6,12 @@ import os import sys +import json import logging import logging.handlers import argparse +from .settings import Settings, settings, os_settings, dict_settings from .debugging import bacpypes_debugging, LoggingFormatter, ModuleLogger from configparser import ConfigParser as _ConfigParser @@ -18,13 +20,6 @@ _debug = 0 _log = ModuleLogger(globals()) -# configuration -BACPYPES_INI = os.getenv('BACPYPES_INI', 'BACpypes.ini') -BACPYPES_DEBUG = os.getenv('BACPYPES_DEBUG', '') -BACPYPES_COLOR = os.getenv('BACPYPES_COLOR', None) -BACPYPES_MAXBYTES = int(os.getenv('BACPYPES_MAXBYTES', 1048576)) -BACPYPES_BACKUPCOUNT = int(os.getenv('BACPYPES_BACKUPCOUNT', 5)) - # # ConsoleLogHandler # @@ -83,6 +78,7 @@ class ArgumentParser(argparse.ArgumentParser): --buggers list the debugging logger names --debug [DEBUG [DEBUG ...]] attach a handler to loggers --color debug in color + --route-aware turn on route aware """ def __init__(self, **kwargs): @@ -90,6 +86,10 @@ def __init__(self, **kwargs): if _debug: ArgumentParser._debug("__init__") argparse.ArgumentParser.__init__(self, **kwargs) + # load settings from the environment + self.update_os_env() + if _debug: ArgumentParser._debug(" - os environment") + # add a way to get a list of the debugging hooks self.add_argument("--buggers", help="list the debugging logger names", @@ -105,8 +105,24 @@ def __init__(self, **kwargs): self.add_argument("--color", help="turn on color debugging", action="store_true", + default=None, ) + # add a way to turn on route aware + self.add_argument("--route-aware", + help="turn on route aware", + action="store_true", + default=None, + ) + + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: ArgumentParser._debug("update_os_env") + + # use settings function + os_settings() + if _debug: ArgumentParser._debug(" - settings: %r", settings) + def parse_args(self, *args, **kwargs): """Parse the arguments as usual, then add default processing.""" if _debug: ArgumentParser._debug("parse_args") @@ -114,6 +130,51 @@ def parse_args(self, *args, **kwargs): # pass along to the parent class result_args = argparse.ArgumentParser.parse_args(self, *args, **kwargs) + # update settings + self.expand_args(result_args) + if _debug: ArgumentParser._debug(" - args expanded") + + # add debugging loggers + self.interpret_debugging(result_args) + if _debug: ArgumentParser._debug(" - interpreted debugging") + + # return what was parsed and expanded + return result_args + + def expand_args(self, result_args): + """Expand the arguments and/or update the settings.""" + if _debug: ArgumentParser._debug("expand_args %r", result_args) + + # check for debug + if result_args.debug is None: + if _debug: ArgumentParser._debug(" - debug not specified") + elif not result_args.debug: + if _debug: ArgumentParser._debug(" - debug with no args") + settings.debug.update(["__main__"]) + else: + if _debug: ArgumentParser._debug(" - debug: %r", result_args.debug) + settings.debug.update(result_args.debug) + + # check for color + if result_args.color is None: + if _debug: ArgumentParser._debug(" - color not specified") + else: + if _debug: ArgumentParser._debug(" - color: %r", result_args.color) + settings.color = result_args.color + + # check for route aware + if result_args.route_aware is None: + if _debug: ArgumentParser._debug(" - route_aware not specified") + else: + if _debug: ArgumentParser._debug(" - route_aware: %r", result_args.route_aware) + settings.route_aware = result_args.route_aware + + def interpret_debugging(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: + ArgumentParser._debug("interpret_debugging %r", result_args) + ArgumentParser._debug(" - settings: %r", settings) + # check to dump labels if result_args.buggers: loggers = sorted(logging.Logger.manager.loggerDict.keys()) @@ -121,47 +182,37 @@ def parse_args(self, *args, **kwargs): sys.stdout.write(loggerName + '\n') sys.exit(0) - # check for debug - if result_args.debug is None: - # --debug not specified - result_args.debug = [] - elif not result_args.debug: - # --debug, but no arguments - result_args.debug = ["__main__"] - - # check for debugging from the environment - if BACPYPES_DEBUG: - result_args.debug.extend(BACPYPES_DEBUG.split()) - if BACPYPES_COLOR: - result_args.color = True - # keep track of which files are going to be used file_handlers = {} # loop through the bug list - for i, debug_name in enumerate(result_args.debug): - color = (i % 6) + 2 if result_args.color else None + for i, debug_name in enumerate(settings.debug): + color = (i % 6) + 2 if settings.color else None debug_specs = debug_name.split(':') - if len(debug_specs) == 1: + if (len(debug_specs) == 1) and (not settings.debug_file): ConsoleLogHandler(debug_name, color=color) else: # the debugger name is just the first component - debug_name = debug_specs[0] + debug_name = debug_specs.pop(0) + + if debug_specs: + file_name = debug_specs.pop(0) + else: + file_name = settings.debug_file # if the file is already being used, use the already created handler - file_name = debug_specs[1] if file_name in file_handlers: handler = file_handlers[file_name] else: - if len(debug_specs) >= 3: - maxBytes = int(debug_specs[2]) + if debug_specs: + maxBytes = int(debug_specs.pop(0)) else: - maxBytes = BACPYPES_MAXBYTES - if len(debug_specs) >= 4: - backupCount = int(debug_specs[3]) + maxBytes = settings.max_bytes + if debug_specs: + backupCount = int(debug_specs.pop(0)) else: - backupCount = BACPYPES_BACKUPCOUNT + backupCount = settings.backup_count # create a handler handler = logging.handlers.RotatingFileHandler( @@ -187,7 +238,7 @@ class ConfigArgumentParser(ArgumentParser): """ ConfigArgumentParser extends the ArgumentParser with the functionality to - read in a configuration file. + read in an INI configuration file. --ini INI provide a separate INI file """ @@ -200,15 +251,22 @@ def __init__(self, **kwargs): # add a way to read a configuration file self.add_argument('--ini', help="device object configuration file", - default=BACPYPES_INI, + default=settings.ini, ) - def parse_args(self, *args, **kwargs): - """Parse the arguments as usual, then add default processing.""" - if _debug: ConfigArgumentParser._debug("parse_args") + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: ConfigArgumentParser._debug("update_os_env") - # pass along to the parent class - result_args = ArgumentParser.parse_args(self, *args, **kwargs) + # start with normal env vars + ArgumentParser.update_os_env(self) + + # provide a default value for the INI file name + settings["ini"] = os.getenv("BACPYPES_INI", "BACpypes.ini") + + def expand_args(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: ConfigArgumentParser._debug("expand_args %r", result_args) # read in the configuration file config = _ConfigParser() @@ -220,12 +278,70 @@ def parse_args(self, *args, **kwargs): raise RuntimeError("INI file with BACpypes section required") # convert the contents to an object - ini_obj = type('ini', (object,), dict(config.items('BACpypes'))) + ini_obj = Settings(dict(config.items('BACpypes'))) if _debug: _log.debug(" - ini_obj: %r", ini_obj) # add the object to the parsed arguments setattr(result_args, 'ini', ini_obj) - # return what was parsed - return result_args + # continue with normal expansion + ArgumentParser.expand_args(self, result_args) + +# +# JSONArgumentParser +# + + +@bacpypes_debugging +class JSONArgumentParser(ArgumentParser): + + """ + JSONArgumentParser extends the ArgumentParser with the functionality to + read in a JSON configuration file. + + --json JSON provide a separate JSON file + """ + + def __init__(self, **kwargs): + """Follow normal initialization and add BACpypes arguments.""" + if _debug: JSONArgumentParser._debug("__init__") + ArgumentParser.__init__(self, **kwargs) + + # add a way to read a configuration file + self.add_argument('--json', + help="configuration file", + default=settings.json, + ) + + def update_os_env(self): + """Update the settings with values from the environment, if provided.""" + if _debug: JSONArgumentParser._debug("update_os_env") + + # start with normal env vars + ArgumentParser.update_os_env(self) + + # provide a default value for the INI file name + settings["json"] = os.getenv("BACPYPES_JSON", "BACpypes.json") + + def expand_args(self, result_args): + """Take the result of parsing the args and interpret them.""" + if _debug: JSONArgumentParser._debug("expand_args %r", result_args) + + # read in the settings file + try: + with open(result_args.json) as json_file: + json_obj = json.load(json_file, object_hook=Settings) + if _debug: JSONArgumentParser._debug(" - json_obj: %r", json_obj) + except FileNotFoundError: + raise RuntimeError("settings file not found: %r\n" % (settings.json,)) + + # look for settings + if "bacpypes" in json_obj: + dict_settings(**json_obj.bacpypes) + if _debug: JSONArgumentParser._debug(" - settings: %r", settings) + + # add the object to the parsed arguments + setattr(result_args, 'json', json_obj) + # continue with normal expansion + ArgumentParser.expand_args(self, result_args) diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py index 88d7ba99..c22608f6 100644 --- a/py34/bacpypes/local/device.py +++ b/py34/bacpypes/local/device.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -from ..debugging import bacpypes_debugging, ModuleLogger +from ..debugging import bacpypes_debugging, ModuleLogger, xtob from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ OctetString, CharacterString, BitString, Enumerated, Date, Time, \ diff --git a/py34/bacpypes/local/file.py b/py34/bacpypes/local/file.py index 8007bef4..6afe8495 100644 --- a/py34/bacpypes/local/file.py +++ b/py34/bacpypes/local/file.py @@ -1,16 +1,9 @@ #!/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()) diff --git a/py34/bacpypes/local/object.py b/py34/bacpypes/local/object.py index 3078b9c6..42cb78f4 100644 --- a/py34/bacpypes/local/object.py +++ b/py34/bacpypes/local/object.py @@ -1,12 +1,26 @@ #!/usr/bin/env python +import re + from ..debugging import bacpypes_debugging, ModuleLogger -from ..basetypes import PropertyIdentifier -from ..constructeddata import ArrayOf +from ..task import OneShotTask +from ..primitivedata import Atomic, Null, BitString, CharacterString, \ + Date, Integer, Double, Enumerated, OctetString, Real, Time, Unsigned +from ..basetypes import PropertyIdentifier, DateTime, NameValue, BinaryPV, \ + ChannelValue, DoorValue, PriorityValue, PriorityArray +from ..constructeddata import Array, ArrayOf, SequenceOf from ..errors import ExecutionError -from ..object import Property, Object +from ..object import Property, ReadableProperty, WritableProperty, OptionalProperty, Object, \ + AccessDoorObject, AnalogOutputObject, AnalogValueObject, \ + BinaryOutputObject, BinaryValueObject, BitStringValueObject, CharacterStringValueObject, \ + DateValueObject, DatePatternValueObject, DateTimePatternValueObject, \ + DateTimeValueObject, IntegerValueObject, \ + LargeAnalogValueObject, LightingOutputObject, MultiStateOutputObject, \ + MultiStateValueObject, OctetStringValueObject, PositiveIntegerValueObject, \ + TimeValueObject, TimePatternValueObject, ChannelObject + # some debugging _debug = 0 @@ -66,3 +80,805 @@ class CurrentPropertyListMixIn(Object): CurrentPropertyList(), ] +# +# Turtle Reference Patterns +# + +# character reference patterns +HEX = u"[0-9A-Fa-f]" +PERCENT = u"%" + HEX + HEX +UCHAR = u"[\\\]u" + HEX * 4 + "|" + u"[\\\]U" + HEX * 8 + +# character sets +PN_CHARS_BASE = ( + u"A-Za-z" + u"\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF" + u"\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF" + u"\uFDF0-\uFFFD\U00010000-\U000EFFFF" +) + +PN_CHARS_U = PN_CHARS_BASE + u"_" +PN_CHARS = u"-" + PN_CHARS_U + u"0-9\u00B7\u0300-\u036F\u203F-\u2040" + +# patterns +IRIREF = u'[<]([^\u0000-\u0020<>"{}|^`\\\]|' + UCHAR + u")*[>]" +PN_PREFIX = u"[" + PN_CHARS_BASE + u"](([." + PN_CHARS + u"])*[" + PN_CHARS + u"])?" + +PN_LOCAL_ESC = u"[-\\_~.!$&'()*+,;=/?#@%]" +PLX = u"(" + PERCENT + u"|" + PN_LOCAL_ESC + u")" + +# non-prefixed names +PN_LOCAL = ( + u"([" + + PN_CHARS_U + + u":0-9]|" + + PLX + + u")(([" + + PN_CHARS + + u".:]|" + + PLX + + u")*([" + + PN_CHARS + + u":]|" + + PLX + + u"))?" +) + +# namespace prefix declaration +PNAME_NS = u"(" + PN_PREFIX + u")?:" + +# prefixed names +PNAME_LN = PNAME_NS + PN_LOCAL + +# blank nodes +BLANK_NODE_LABEL = ( + u"_:[" + PN_CHARS_U + u"0-9]([" + PN_CHARS + u".]*[" + PN_CHARS + u"])?" +) + +# see https://www.w3.org/TR/turtle/#sec-parsing-terms +iriref_re = re.compile(u"^" + IRIREF + u"$", re.UNICODE) +local_name_re = re.compile(u"^" + PN_LOCAL + u"$", re.UNICODE) +namespace_prefix_re = re.compile(u"^" + PNAME_NS + u"$", re.UNICODE) +prefixed_name_re = re.compile(u"^" + PNAME_LN + u"$", re.UNICODE) +blank_node_re = re.compile(u"^" + BLANK_NODE_LABEL + u"$", re.UNICODE) + +# see https://tools.ietf.org/html/bcp47#section-2.1 for better syntax +language_tag_re = re.compile(u"^[A-Za-z0-9-]+$", re.UNICODE) + +class IRI: + # regex from RFC 3986 + _e = r"^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?" + _p = re.compile(_e) + _default_ports = (("http", ":80"), ("https", ":443")) + + def __init__(self, iri=None): + self.iri = iri + + if not iri: + g = (None, None, None, None, None) + else: + m = IRI._p.match(iri) + if not m: + raise ValueError("not an IRI") + + # remove default http and https ports + g = list(m.groups()) + for scheme, suffix in IRI._default_ports: + if (g[0] == scheme) and g[1] and g[1].endswith(suffix): + g[1] = g[1][: g[1].rfind(":")] + break + + self.scheme, self.authority, self.path, self.query, self.fragment = g + + def __str__(self): + rval = "" + if self.scheme: + rval += self.scheme + ":" + if self.authority is not None: + rval += "//" + self.authority + if self.path is not None: + rval += self.path + if self.query is not None: + rval += "?" + self.query + if self.fragment is not None: + rval += "#" + self.fragment + return rval + + def is_local_name(self): + if not all( + ( + self.scheme is None, + self.authority is None, + self.path, + self.query is None, + self.fragment is None, + ) + ): + return False + if self.path.startswith(":") or "/" in self.path: # term is not ':x' + return False + return True + + def is_prefix(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path == "" # term is 'x:' + else: + return self.path == ":" # term is ':' + + def is_prefixed_name(self): + if not all((self.authority is None, self.query is None, self.fragment is None)): + return False + if self.scheme: + return self.path != "" # term is 'x:y' + else: # term is ':y' but not ':' + return self.path and (self.path != ":") and self.path.startswith(":") + + def resolve(self, iri): + """Resolve a relative IRI to this IRI as a base.""" + # parse the IRI if necessary + if isinstance(iri, str): + iri = IRI(iri) + elif not isinstance(iri, IRI): + raise TypeError("iri must be an IRI or a string") + + # return an IRI object + rslt = IRI() + + if iri.scheme and iri.scheme != self.scheme: + rslt.scheme = iri.scheme + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.scheme = self.scheme + + if iri.authority is not None: + rslt.authority = iri.authority + rslt.path = iri.path + rslt.query = iri.query + else: + rslt.authority = self.authority + + if not iri.path: + rslt.path = self.path + if iri.query is not None: + rslt.query = iri.query + else: + rslt.query = self.query + else: + if iri.path.startswith("/"): + # IRI represents an absolute path + rslt.path = iri.path + else: + # merge paths + path = self.path + + # append relative path to the end of the last + # directory from base + path = path[0 : path.rfind("/") + 1] + if len(path) > 0 and not path.endswith("/"): + path += "/" + path += iri.path + + rslt.path = path + + rslt.query = iri.query + + # normalize path + if rslt.path != "": + rslt.remove_dot_segments() + + rslt.fragment = iri.fragment + + return rslt + + def remove_dot_segments(self): + # empty path shortcut + if len(self.path) == 0: + return + + input_ = self.path.split("/") + output_ = [] + + while len(input_) > 0: + next = input_.pop(0) + done = len(input_) == 0 + + if next == ".": + if done: + # ensure output has trailing / + output_.append("") + continue + + if next == "..": + if len(output_) > 0: + output_.pop() + if done: + # ensure output has trailing / + output_.append("") + continue + + output_.append(next) + + # ensure output has leading / + if len(output_) > 0 and output_[0] != "": + output_.insert(0, "") + if len(output_) == 1 and output_[0] == "": + return "/" + + self.path = "/".join(output_) + + +@bacpypes_debugging +class TagSet: + def index(self, name, value=None): + """Find the first name with dictionary semantics or (name, value) with + list semantics.""" + if _debug: TagSet._debug("index %r %r", name, value) + + # if this is a NameValue rip it apart first + if isinstance(name, NameValue): + name, value = name.name, name.value + + # no value then look for first matching name + if value is None: + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if name == v.name: + return i + else: + raise KeyError(name) + + # skip int values, it is the zeroth element of an array but does + # not exist for a list + for i, v in enumerate(self.value): + if isinstance(v, int): + continue + if ( + name == v.name + and isinstance(value, type(v.value)) + and value.value == v.value.value + ): + return i + else: + raise ValueError((name, value)) + + def add(self, name, value=None): + """Add a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("add %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + # name is a string + if not isinstance(name, str): + raise TypeError("name must be a string, got %r" % (type(name),)) + + # reserved directive names + if name.startswith("@"): + if name == "@base": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@base') + if v and v.value == value.value: + pass + else: + raise ValueError("@base exists") + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + elif name == "@id": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@id') + if v and v.value == value.value: + pass + else: + raise ValueError("@id exists") + +# # check the patterns +# for pattern in (blank_node_re, prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(value.value): +# break +# else: +# raise ValueError("invalid value for @id") + + elif name == "@language": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get("@language") + if v and v.value == value.value: + pass + else: + raise ValueError("@language exists") + + if not language_tag_re.match(value.value): + raise ValueError("value must be a language tag") + + elif name == "@vocab": + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get('@vocab') + if v and v.value == value.value: + pass + else: + raise ValueError("@vocab exists") + + else: + raise ValueError("invalid directive name") + + elif name.endswith(":"): + if not isinstance(value, CharacterString): + raise TypeError("value must be an string") + + v = self.get(name) + if v and v.value == value.value: + pass + else: + raise ValueError("prefix exists: %r" % (name,)) + +# if not iriref_re.match(value.value): +# raise ValueError("value must be an IRI") + + else: +# # check the patterns +# for pattern in (prefixed_name_re, local_name_re, iriref_re): +# if pattern.match(name): +# break +# else: +# raise ValueError("invalid name") + pass + + # check the value + if not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # see if the (name, value) already exists + try: + self.index(name, value) + except ValueError: + super(TagSet, self).append(NameValue(name=name, value=value)) + + def discard(self, name, value=None): + """Discard a (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("discard %r %r", name, value) + + # provide a Null if you are adding a is-a relationship, wrap strings + # to be friendly + if value is None: + value = Null() + elif isinstance(value, str): + value = CharacterString(value) + + indx = self.index(name, value) + return super(TagSet, self).__delitem__(indx) + + def append(self, name_value): + """Override the append operation for mutable set semantics.""" + if _debug: TagSet._debug("append %r", name_value) + + if not isinstance(name_value, NameValue): + raise TypeError + + # turn this into an add operation + self.add(name_value.name, name_value.value) + + def get(self, key, default=None): + """Get the value of a key or default value if the key was not found, + dictionary semantics.""" + if _debug: TagSet._debug("get %r %r", key, default) + + try: + if not isinstance(key, str): + raise TypeError(key) + return self.value[self.index(key)].value + except KeyError: + return default + + def __getitem__(self, item): + """If item is an integer, return the value of the NameValue element + with array/sequence semantics. If the item is a string, return the + value with dictionary semantics.""" + if _debug: TagSet._debug("__getitem__ %r", item) + + # integers imply index + if isinstance(item, int): + return super(TagSet, self).__getitem__(item) + + return self.value[self.index(item)] + + def __setitem__(self, item, value): + """If item is an integer, change the value of the NameValue element + with array/sequence semantics. If the item is a string, change the + current value or add a new value with dictionary semantics.""" + if _debug: TagSet._debug("__setitem__ %r %r", item, value) + + # integers imply index + if isinstance(item, int): + indx = item + if indx < 0: + raise IndexError("assignment index out of range") + elif isinstance(self, Array): + if indx == 0 or indx > len(self.value): + raise IndexError + elif indx >= len(self.value): + raise IndexError + elif isinstance(item, str): + try: + indx = self.index(item) + except KeyError: + self.add(item, value) + return + else: + raise TypeError(repr(item)) + + # check the value + if value is None: + value = Null() + elif not isinstance(value, (Atomic, DateTime)): + raise TypeError("invalid value") + + # now we're good to go + self.value[indx].value = value + + def __delitem__(self, item): + """If the item is a integer, delete the element with array semantics, or + if the item is a string, delete the element with dictionary semantics, + or (name, value) with mutable set semantics.""" + if _debug: TagSet._debug("__delitem__ %r", item) + + # integers imply index + if isinstance(item, int): + indx = item + elif isinstance(item, str): + indx = self.index(item) + elif isinstance(item, tuple): + indx = self.index(*item) + else: + raise TypeError(item) + + return super(TagSet, self).__delitem__(indx) + + def __contains__(self, key): + if _debug: TagSet._debug("__contains__ %r", key) + + try: + if isinstance(key, tuple): + self.index(*key) + elif isinstance(key, str): + self.index(key) + else: + raise TypeError(key) + + return True + except (KeyError, ValueError): + return False + + +class ArrayOfNameValue(TagSet, ArrayOf(NameValue)): + pass + + +class SequenceOfNameValue(TagSet, SequenceOf(NameValue)): + pass + + +class TagsMixIn(Object): + properties = \ + [ OptionalProperty('tags', ArrayOfNameValue) + ] + + +@bacpypes_debugging +def Commandable(datatype, presentValue='presentValue', priorityArray='priorityArray', relinquishDefault='relinquishDefault'): + if _debug: Commandable._debug("Commandable %r ...", datatype) + + class _Commando(object): + + properties = [ + WritableProperty(presentValue, datatype), + ReadableProperty(priorityArray, PriorityArray), + ReadableProperty(relinquishDefault, datatype), + ] + + _pv_choice = None + + def __init__(self, **kwargs): + if _debug: Commandable._debug("_Commando.__init__(%r, %r, %r, %r) %r", datatype, presentValue, priorityArray, relinquishDefault, kwargs) + super(_Commando, self).__init__(**kwargs) + + # build a default value in case one is needed + default_value = datatype().value + if issubclass(datatype, Enumerated): + default_value = datatype._xlate_table[default_value] + if _debug: Commandable._debug(" - default_value: %r", default_value) + + # see if a present value was provided + if (presentValue not in kwargs): + setattr(self, presentValue, default_value) + + # see if a priority array was provided + if (priorityArray not in kwargs): + setattr(self, priorityArray, PriorityArray()) + + # see if a present value was provided + if (relinquishDefault not in kwargs): + setattr(self, relinquishDefault, default_value) + + def _highest_priority_value(self): + if _debug: Commandable._debug("_highest_priority_value") + + priority_array = getattr(self, priorityArray) + for i in range(1, 17): + priority_value = priority_array[i] + if priority_value.null is None: + if _debug: Commandable._debug(" - found at index: %r", i) + + value = getattr(priority_value, _Commando._pv_choice) + value_source = "###" + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + break + else: + value = getattr(self, relinquishDefault) + value_source = None + + if _debug: Commandable._debug(" - value, value_source: %r, %r", value, value_source) + + # return what you found + return value, value_source + + def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct=False): + if _debug: Commandable._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", property, value, arrayIndex, priority, direct) + + # when writing to the presentValue with a priority + if (property == presentValue): + if _debug: Commandable._debug(" - writing to %s, priority %r", presentValue, priority) + + # default (lowest) priority + if priority is None: + priority = 16 + if _debug: Commandable._debug(" - translate to priority array, index %d", priority) + + # translate to updating the priority array + property = priorityArray + arrayIndex = priority + priority = None + + # update the priority array entry + if (property == priorityArray): + if (arrayIndex is None): + if _debug: Commandable._debug(" - writing entire %s", priorityArray) + + # pass along the request + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + else: + if _debug: Commandable._debug(" - writing to %s, array index %d", priorityArray, arrayIndex) + + # check the bounds + if arrayIndex == 0: + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + if (arrayIndex < 1) or (arrayIndex > 16): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + + # update the specific priorty value element + priority_value = getattr(self, priorityArray)[arrayIndex] + if _debug: Commandable._debug(" - priority_value: %r", priority_value) + + # the null or the choice has to be set, the other clear + if value is (): + if _debug: Commandable._debug(" - write a null") + priority_value.null = value + setattr(priority_value, _Commando._pv_choice, None) + else: + if _debug: Commandable._debug(" - write a value") + + if issubclass(datatype, Enumerated): + value = datatype._xlate_table[value] + if _debug: Commandable._debug(" - remapped enumeration: %r", value) + + priority_value.null = None + setattr(priority_value, _Commando._pv_choice, value) + + # look for the highest priority value + value, value_source = self._highest_priority_value() + + # compare with the current value + current_value = getattr(self, presentValue) + if value == current_value: + if _debug: Commandable._debug(" - no present value change") + return + + # turn this into a present value change + property = presentValue + arrayIndex = priority = None + + # allow the request to pass through + if _debug: Commandable._debug(" - super: %r %r arrayIndex=%r priority=%r", property, value, arrayIndex, priority) + + super(_Commando, self).WriteProperty( + property, value, + arrayIndex=arrayIndex, priority=priority, direct=direct, + ) + + # look up a matching priority value choice + for element in PriorityValue.choiceElements: + if issubclass(datatype, element.klass): + _Commando._pv_choice = element.name + break + else: + _Commando._pv_choice = 'constructedValue' + if _debug: Commandable._debug(" - _pv_choice: %r", _Commando._pv_choice) + + # return the class + return _Commando + +# +# MinOnOffTask +# + +@bacpypes_debugging +class MinOnOffTask(OneShotTask): + + def __init__(self, binary_obj): + if _debug: MinOnOffTask._debug("__init__ %s", repr(binary_obj)) + OneShotTask.__init__(self) + + # save a reference to the object + self.binary_obj = binary_obj + + # listen for changes to the present value + self.binary_obj._property_monitors['presentValue'].append(self.present_value_change) + + def present_value_change(self, old_value, new_value): + if _debug: MinOnOffTask._debug("present_value_change %r %r", old_value, new_value) + + # if there's no value change, skip all this + if old_value == new_value: + if _debug: MinOnOffTask._debug(" - no state change") + return + + # get the minimum on/off time + if new_value == 'inactive': + task_delay = getattr(self.binary_obj, 'minimumOnTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum on: %r", task_delay) + elif new_value == 'active': + task_delay = getattr(self.binary_obj, 'minimumOffTime') or 0 + if _debug: MinOnOffTask._debug(" - minimum off: %r", task_delay) + else: + raise ValueError("unrecognized present value for %r: %r" % (self.binary_obj.objectIdentifier, new_value)) + + # if there's no delay, don't bother + if not task_delay: + if _debug: MinOnOffTask._debug(" - no delay") + return + + # set the value at priority 6 + self.binary_obj.WriteProperty('presentValue', new_value, priority=6) + + # install this to run, if there is a delay + self.install_task(delta=task_delay) + + def process_task(self): + if _debug: MinOnOffTask._debug("process_task(%s)", self.binary_obj.objectName) + + # clear the value at priority 6 + self.binary_obj.WriteProperty('presentValue', (), priority=6) + +# +# MinOnOff +# + +@bacpypes_debugging +class MinOnOff(object): + + def __init__(self, **kwargs): + if _debug: MinOnOff._debug("__init__ ...") + super(MinOnOff, self).__init__(**kwargs) + + # create the timer task + self._min_on_off_task = MinOnOffTask(self) + +# +# Commandable Standard Objects +# + +class AccessDoorCmdObject(Commandable(DoorValue), AccessDoorObject): + pass + +class AnalogOutputCmdObject(Commandable(Real), AnalogOutputObject): + pass + +class AnalogValueCmdObject(Commandable(Real), AnalogValueObject): + pass + +### class BinaryLightingOutputCmdObject(Commandable(Real), BinaryLightingOutputObject): +### pass + +class BinaryOutputCmdObject(Commandable(BinaryPV), MinOnOff, BinaryOutputObject): + pass + +class BinaryValueCmdObject(Commandable(BinaryPV), MinOnOff, BinaryValueObject): + pass + +class BitStringValueCmdObject(Commandable(BitString), BitStringValueObject): + pass + +class CharacterStringValueCmdObject(Commandable(CharacterString), CharacterStringValueObject): + pass + +class DateValueCmdObject(Commandable(Date), DateValueObject): + pass + +class DatePatternValueCmdObject(Commandable(Date), DatePatternValueObject): + pass + +class DateTimeValueCmdObject(Commandable(DateTime), DateTimeValueObject): + pass + +class DateTimePatternValueCmdObject(Commandable(DateTime), DateTimePatternValueObject): + pass + +class IntegerValueCmdObject(Commandable(Integer), IntegerValueObject): + pass + +class LargeAnalogValueCmdObject(Commandable(Double), LargeAnalogValueObject): + pass + +class LightingOutputCmdObject(Commandable(Real), LightingOutputObject): + pass + +class MultiStateOutputCmdObject(Commandable(Unsigned), MultiStateOutputObject): + pass + +class MultiStateValueCmdObject(Commandable(Unsigned), MultiStateValueObject): + pass + +class OctetStringValueCmdObject(Commandable(OctetString), OctetStringValueObject): + pass + +class PositiveIntegerValueCmdObject(Commandable(Unsigned), PositiveIntegerValueObject): + pass + +class TimeValueCmdObject(Commandable(Time), TimeValueObject): + pass + +class TimePatternValueCmdObject(Commandable(Time), TimePatternValueObject): + pass + +@bacpypes_debugging +class ChannelValueProperty(Property): + + def __init__(self): + if _debug: ChannelValueProperty._debug("__init__") + Property.__init__(self, 'presentValue', ChannelValue, default=None, optional=False, mutable=True) + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: ChannelValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + + ### Clause 12.53.5, page 487 + raise NotImplementedError() + +class ChannelCmdObject(ChannelObject): + + properties = [ + ChannelValueProperty(), + ] diff --git a/py34/bacpypes/local/schedule.py b/py34/bacpypes/local/schedule.py index d22911a3..91d6ccf7 100644 --- a/py34/bacpypes/local/schedule.py +++ b/py34/bacpypes/local/schedule.py @@ -4,7 +4,6 @@ Local Schedule Object """ -import sys import calendar from time import mktime as _mktime diff --git a/py34/bacpypes/netservice.py b/py34/bacpypes/netservice.py index bfb44a77..a59d4a5e 100755 --- a/py34/bacpypes/netservice.py +++ b/py34/bacpypes/netservice.py @@ -6,14 +6,17 @@ from copy import deepcopy as _deepcopy +from .settings import settings from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .errors import ConfigurationError +from .core import deferred from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement from .task import FunctionTask -from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation +from .pdu import Address, LocalBroadcast, LocalStation, PDU, RemoteStation, \ + GlobalBroadcast from .npdu import NPDU, npdu_types, IAmRouterToNetwork, WhoIsRouterToNetwork, \ WhatIsNetworkNumber, NetworkNumberIs from .apdu import APDU as _APDU @@ -26,7 +29,7 @@ ROUTER_AVAILABLE = 0 # normal ROUTER_BUSY = 1 # router is busy ROUTER_DISCONNECTED = 2 # could make a connection, but hasn't -ROUTER_UNREACHABLE = 3 # cannot route +ROUTER_UNREACHABLE = 3 # temporarily unreachable # # RouterInfo @@ -36,13 +39,17 @@ class RouterInfo(DebugContents): """These objects are routing information records that map router addresses with destination networks.""" - _debug_contents = ('snet', 'address', 'dnets', 'status') + _debug_contents = ('snet', 'address', 'dnets') - def __init__(self, snet, address, dnets, status=ROUTER_AVAILABLE): + def __init__(self, snet, address): 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 + self.dnets = {} # {dnet: status} + + def set_status(self, dnets, status): + """Change the status of each of the DNETS.""" + for dnet in dnets: + self.dnets[dnet] = status # # RouterInfoCache @@ -54,109 +61,121 @@ 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) + self.routers = {} # snet -> {Address: RouterInfo} + self.path_info = {} # (snet, dnet) -> RouterInfo - # check to see if we know about it - if dnet not in self.networks: - if _debug: RouterInfoCache._debug(" - no route") - return None + def get_router_info(self, snet, dnet): + if _debug: RouterInfoCache._debug("get_router_info %r %r", snet, dnet) # return the network and address - router_info = self.networks[dnet] + router_info = self.path_info.get((snet, dnet), None) 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) + return router_info - def update_router_info(self, snet, address, dnets): + def update_router_info(self, snet, address, dnets, status=ROUTER_AVAILABLE): 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] + existing_router_info = self.routers.get(snet, {}).get(address, None) - # add (or move) the destination networks + other_routers = set() 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") + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + if other_routers: + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + # update current router info if there is one + if not existing_router_info: + router_info = RouterInfo(snet, address) + if snet not in self.routers: + self.routers[snet] = {address: router_info} + else: + self.routers[snet][address] = router_info - # 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) + for dnet in dnets: + self.path_info[(snet, dnet)] = router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r via %r", snet, dnet, router_info.address) + router_info.dnets[dnet] = status + else: + for dnet in dnets: + if dnet not in existing_router_info.dnets: + self.path_info[(snet, dnet)] = existing_router_info + if _debug: RouterInfoCache._debug(" - add path: %r -> %r", snet, dnet) + existing_router_info.dnets[dnet] = status 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") + existing_router_info = self.routers.get(snet, {}).get(address, None) + if not existing_router_info: + if _debug: RouterInfoCache._debug(" - not a router we know about") return - router_info = self.routers[key] - router_info.status = status - if _debug: RouterInfoCache._debug(" - status updated") + existing_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 + if (address is None) and (dnets is None): + raise RuntimeError("inconsistent parameters") - # look up the router reference - key = (snet, address) - if key not in self.routers: - if _debug: RouterInfoCache._debug(" - unknown router") + # remove the dnets from a router or the whole router + if (address is not None): + router_info = self.routers.get(snet, {}).get(address, None) + if not router_info: + if _debug: RouterInfoCache._debug(" - no route info") + else: + for dnet in (dnets or router_info.dnets): + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + del self.routers[snet][address] return - router_info = self.routers[key] - if _debug: RouterInfoCache._debug(" - router_info: %r", router_info) + # look for routers to the dnets + other_routers = set() + for dnet in dnets: + other_router = self.path_info.get((snet, dnet), None) + if other_router and (other_router is not existing_router_info): + other_routers.add(other_router) + + # remove the dnets from other router(s) and paths + for router_info in other_routers: + for dnet in dnets: + if dnet in router_info.dnets: + del router_info.dnets[dnet] + del self.path_info[(snet, dnet)] + if _debug: RouterInfoCache._debug(" - del path: %r -> %r via %r", snet, dnet, router_info.address) + if not router_info.dnets: + del self.routers[snet][router_info.address] + if _debug: RouterInfoCache._debug(" - no dnets: %r via %r", snet, router_info.address) + + def update_source_network(self, old_snet, new_snet): + if _debug: RouterInfoCache._debug("update_source_network %r %r", old_snet, new_snet) + + if old_snet not in self.routers: + if _debug: RouterInfoCache._debug(" - no router references: %r", list(self.routers.keys())) + return - # if dnets is None, remove all the networks for the router - if dnets is None: - dnets = router_info.dnets + # move the router info records to the new net + snet_routers = self.routers[new_snet] = self.routers.pop(old_snet) - # 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] + # update the paths + for address, router_info in snet_routers.items(): + for dnet in router_info.dnets: + self.path_info[(new_snet, dnet)] = self.path_info.pop((old_snet, dnet)) # # NetworkAdapter @@ -165,13 +184,19 @@ def delete_router_info(self, snet, address=None, dnets=None): @bacpypes_debugging class NetworkAdapter(Client, DebugContents): - _debug_contents = ('adapterSAP-', 'adapterNet', 'adapterNetConfigured') + _debug_contents = ( + 'adapterSAP-', + 'adapterNet', + 'adapterAddr', + 'adapterNetConfigured', + ) - def __init__(self, sap, net, cid=None): - if _debug: NetworkAdapter._debug("__init__ %s %r cid=%r", sap, net, cid) + def __init__(self, sap, net, addr, cid=None): + if _debug: NetworkAdapter._debug("__init__ %s %r %r cid=%r", sap, net, addr, cid) Client.__init__(self, cid) self.adapterSAP = sap self.adapterNet = net + self.adapterAddr = addr # record if this was 0=learned, 1=configured, None=unknown if net is None: @@ -209,7 +234,7 @@ def DisconnectConnectionToNetwork(self, net): class NetworkServiceAccessPoint(ServiceAccessPoint, Server, DebugContents): _debug_contents = ('adapters++', 'pending_nets', - 'local_adapter-', 'local_address', + 'local_adapter-', ) def __init__(self, router_info_cache=None, sap=None, sid=None): @@ -226,36 +251,60 @@ def __init__(self, router_info_cache=None, sap=None, sid=None): # map to a list of application layer packets waiting for a path self.pending_nets = {} - # these are set when bind() is called + # 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.""" + """Create a network adapter object and bind. + + bind(s, None, None) + Called for simple applications, local network unknown, no specific + address, APDUs sent upstream + + bind(s, net, None) + Called for routers, bind to the network, (optionally?) drop APDUs + + bind(s, None, address) + Called for applications or routers, bind to the network (to be + discovered), send up APDUs with a metching address + + bind(s, net, address) + Called for applications or routers, bind to the network, send up + APDUs with a metching address. + """ if _debug: NetworkServiceAccessPoint._debug("bind %r net=%r address=%r", server, net, address) # make sure this hasn't already been called with this network if net in self.adapters: - raise RuntimeError("already bound") + raise RuntimeError("already bound: %r" % (net,)) # create an adapter object, add it to our map - adapter = NetworkAdapter(self, net) + adapter = NetworkAdapter(self, net, address) self.adapters[net] = adapter - if _debug: NetworkServiceAccessPoint._debug(" - adapters[%r]: %r", net, adapter) + if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r, %r", net, adapter) # if the address was given, make it the "local" one - if address and not self.local_address: + if address: + if _debug: NetworkServiceAccessPoint._debug(" - setting local adapter") self.local_adapter = adapter - self.local_address = address + + # if the local adapter isn't set yet, make it the first one, and can + # be overridden by a subsequent call if the address is specified + if not self.local_adapter: + if _debug: NetworkServiceAccessPoint._debug(" - default local adapter") + self.local_adapter = adapter + + if not self.local_adapter.adapterAddr: + if _debug: NetworkServiceAccessPoint._debug(" - no local address") # bind to the server bind(adapter, server) #----- - def add_router_references(self, snet, address, dnets): - """Add/update references to routers.""" - if _debug: NetworkServiceAccessPoint._debug("add_router_references %r %r %r", snet, address, dnets) + def update_router_references(self, snet, address, dnets): + """Update references to routers.""" + if _debug: NetworkServiceAccessPoint._debug("update_router_references %r %r %r", snet, address, dnets) # see if we have an adapter for the snet if snet not in self.adapters: @@ -284,13 +333,9 @@ def indication(self, pdu): if (not self.adapters): raise ConfigurationError("no adapters") - # might be able to relax this restriction - if (len(self.adapters) > 1) and (not self.local_adapter): - raise ConfigurationError("local adapter must be set") - # get the local adapter - adapter = self.local_adapter or self.adapters[None] - if _debug: NetworkServiceAccessPoint._debug(" - adapter: %r", adapter) + local_adapter = self.local_adapter + if _debug: NetworkServiceAccessPoint._debug(" - local_adapter: %r", local_adapter) # build a generic APDU apdu = _APDU(user_data=pdu.pduUserData) @@ -305,14 +350,30 @@ def indication(self, pdu): # the hop count always starts out big npdu.npduHopCount = 255 + # if this is route aware, use it for the destination + if settings.route_aware and npdu.pduDestination.addrRoute: + # always a local station for now, in theory this could also be + # a local braodcast address, remote station, or remote broadcast + # but that is not supported by the patterns + assert npdu.pduDestination.addrRoute.addrType == Address.localStationAddr + if _debug: NetworkServiceAccessPoint._debug(" - routed: %r", npdu.pduDestination.addrRoute) + + if npdu.pduDestination.addrType in (Address.remoteStationAddr, Address.remoteBroadcastAddr, Address.globalBroadcastAddr): + if _debug: NetworkServiceAccessPoint._debug(" - continue DADR: %r", apdu.pduDestination) + npdu.npduDADR = apdu.pduDestination + + npdu.pduDestination = npdu.pduDestination.addrRoute + local_adapter.process_npdu(npdu) + return + # local stations given to local adapter if (npdu.pduDestination.addrType == Address.localStationAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # local broadcast given to local adapter if (npdu.pduDestination.addrType == Address.localBroadcastAddr): - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # global broadcast @@ -331,9 +392,10 @@ def indication(self, pdu): raise RuntimeError("invalid destination address type: %s" % (npdu.pduDestination.addrType,)) dnet = npdu.pduDestination.addrNet + if _debug: NetworkServiceAccessPoint._debug(" - dnet: %r", dnet) # if the network matches the local adapter it's local - if (dnet == adapter.adapterNet): + if (dnet == local_adapter.adapterNet): if (npdu.pduDestination.addrType == Address.remoteStationAddr): if _debug: NetworkServiceAccessPoint._debug(" - mapping remote station to local station") npdu.pduDestination = LocalStation(npdu.pduDestination.addrAddr) @@ -343,7 +405,7 @@ def indication(self, pdu): else: raise RuntimeError("addressing problem") - adapter.process_npdu(npdu) + local_adapter.process_npdu(npdu) return # get it ready to send when the path is found @@ -356,49 +418,50 @@ def indication(self, pdu): self.pending_nets[dnet].append(npdu) return - # check cache for an available path - path_info = self.router_info_cache.get_router_info(dnet) + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break # 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) + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - router_info found: %r", router_info) - # 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) + ### check the path status + dnet_status = router_info.dnets[dnet] + if _debug: NetworkServiceAccessPoint._debug(" - dnet_status: %r", dnet_status) # fix the destination - npdu.pduDestination = address + npdu.pduDestination = router_info.address # send it along - adapter.process_npdu(npdu) - return + snet_adapter.process_npdu(npdu) - if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") + else: + if _debug: NetworkServiceAccessPoint._debug(" - no known path to network") - # 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) + # 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() + # 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 adapter in self.adapters.values(): - ### make sure the adapter is OK - self.sap_indication(adapter, xnpdu) + # send it to all of the adapters + for adapter in self.adapters.values(): + self.sap_indication(adapter, xnpdu) def process_npdu(self, adapter, npdu): if _debug: NetworkServiceAccessPoint._debug("process_npdu %r %r", adapter, npdu) # make sure our configuration is OK - if (not self.adapters): + if not self.adapters: raise ConfigurationError("no adapters") # check for source routing @@ -411,29 +474,14 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (1)") return - # 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) - - # see if the router has changed - if not (router_address == npdu.pduSource): - if _debug: NetworkServiceAccessPoint._debug(" - replacing path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) - else: - if _debug: NetworkServiceAccessPoint._debug(" - new path") - - # pass this new path along to the cache - self.router_info_cache.update_router_info(adapter.adapterNet, npdu.pduSource, [snet]) + # 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): if _debug: NetworkServiceAccessPoint._debug(" - no DADR") - processLocally = (not self.local_adapter) or (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) + processLocally = (adapter is self.local_adapter) or (npdu.npduNetMessage is not None) forwardMessage = False elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: @@ -443,8 +491,7 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (2)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) forwardMessage = True elif npdu.npduDADR.addrType == Address.remoteStationAddr: @@ -454,9 +501,8 @@ def process_npdu(self, adapter, npdu): NetworkServiceAccessPoint._warning(" - path error (3)") return - processLocally = self.local_adapter \ - and (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ - and (npdu.npduDADR.addrAddr == self.local_address.addrAddr) + processLocally = (npdu.npduDADR.addrNet == self.local_adapter.adapterNet) \ + and (npdu.npduDADR.addrAddr == self.local_adapter.adapterAddr.addrAddr) forwardMessage = not processLocally elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: @@ -489,29 +535,34 @@ def process_npdu(self, adapter, npdu): 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 ) + apdu.pduSource = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) else: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + apdu.pduSource.addrRoute = npdu.pduSource # map the destination if not npdu.npduDADR: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr elif npdu.npduDADR.addrType == Address.globalBroadcastAddr: - apdu.pduDestination = npdu.npduDADR + apdu.pduDestination = GlobalBroadcast() elif npdu.npduDADR.addrType == Address.remoteBroadcastAddr: apdu.pduDestination = LocalBroadcast() else: - apdu.pduDestination = self.local_address + apdu.pduDestination = self.local_adapter.adapterAddr else: # combine the source address if npdu.npduSADR: apdu.pduSource = npdu.npduSADR + if settings.route_aware: + if _debug: NetworkServiceAccessPoint._debug(" - adding route") + apdu.pduSource.addrRoute = npdu.pduSource else: apdu.pduSource = npdu.pduSource # pass along global broadcast if npdu.npduDADR and npdu.npduDADR.addrType == Address.globalBroadcastAddr: - apdu.pduDestination = npdu.npduDADR + apdu.pduDestination = GlobalBroadcast() else: apdu.pduDestination = npdu.pduDestination if _debug: @@ -605,33 +656,27 @@ def process_npdu(self, adapter, npdu): xadapter.process_npdu(_deepcopy(newpdu)) return - # 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 + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in self.adapters.items(): + router_info = self.router_info_cache.get_router_info(snet, dnet) + if router_info: + break - xadapter = self.adapters[router_net] - if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", xadapter) + # found a path + if router_info: + if _debug: NetworkServiceAccessPoint._debug(" - found path via %r", router_info) # the destination is the address of the router - newpdu.pduDestination = router_address + newpdu.pduDestination = router_info.address # send the packet downstream - xadapter.process_npdu(_deepcopy(newpdu)) + snet_adapter.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 xnpdu = WhoIsRouterToNetwork(dnet) xnpdu.pduDestination = LocalBroadcast() @@ -678,6 +723,8 @@ def sap_confirmation(self, adapter, npdu): @bacpypes_debugging class NetworkServiceElement(ApplicationServiceElement): + _startup_disabled = False + def __init__(self, eid=None): if _debug: NetworkServiceElement._debug("__init__ eid=%r", eid) ApplicationServiceElement.__init__(self, eid) @@ -685,6 +732,49 @@ def __init__(self, eid=None): # network number is timeout self.network_number_is_task = None + # if starting up is enabled defer our startup function + if not self._startup_disabled: + deferred(self.startup) + + def startup(self): + if _debug: NetworkServiceElement._debug("startup") + + # reference the service access point + sap = self.elementService + if _debug: NetworkServiceElement._debug(" - sap: %r", sap) + + # loop through all of the adapters + for adapter in sap.adapters.values(): + if _debug: NetworkServiceElement._debug(" - adapter: %r", adapter) + + if (adapter.adapterNet is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown net") + continue + elif (adapter.adapterAddr is None): + if _debug: NetworkServiceElement._debug(" - skipping, unknown addr") + continue + + # build a list of reachable networks + netlist = [] + + # loop through the adapters + for xadapter in sap.adapters.values(): + if (xadapter is not adapter): + if (xadapter.adapterNet is None) or (xadapter.adapterAddr is None): + continue + netlist.append(xadapter.adapterNet) + + # skip for an empty list, perhaps they are not yet learned + if not netlist: + if _debug: NetworkServiceElement._debug(" - skipping, no netlist") + continue + + # pass this along to the cache -- on hold #213 + # sap.router_info_cache.update_router_info(adapter.adapterNet, adapter.adapterAddr, netlist) + + # send an announcement + self.i_am_router_to_network(adapter=adapter, network=netlist) + def indication(self, adapter, npdu): if _debug: NetworkServiceElement._debug("indication %r %r", adapter, npdu) @@ -765,10 +855,14 @@ def i_am_router_to_network(self, adapter=None, destination=None, network=None): netlist.append(xadapter.adapterNet) ### add the other reachable networks - if network is not None: + if network is None: + pass + elif isinstance(network, int): if network not in netlist: continue netlist = [network] + elif isinstance(network, list): + netlist = [net for net in netlist if net in network] # build a response iamrtn = IAmRouterToNetwork(netlist) @@ -808,7 +902,7 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # add the direct network netlist.append(xadapter.adapterNet) - ### add the other reachable + ### add the other reachable networks? if netlist: if _debug: NetworkServiceElement._debug(" - found these: %r", netlist) @@ -839,51 +933,51 @@ def WhoIsRouterToNetwork(self, adapter, npdu): # send it back self.response(adapter, iamrtn) + return - else: - # see if there is routing information for this source network - router_info = sap.router_info_cache.get_router_info(dnet) + + # look for routing information from the network of one of our + # adapters to the destination network + router_info = None + for snet, snet_adapter in sap.adapters.items(): + router_info = sap.router_info_cache.get_router_info(snet, 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 - if sap.adapters[router_net] is adapter: - if _debug: NetworkServiceElement._debug(" - same network") - return - - # build a response - iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) - iamrtn.pduDestination = npdu.pduSource - - # send it back - self.response(adapter, iamrtn) + break - else: - if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + # found a path + if router_info: + if _debug: NetworkServiceElement._debug(" - router found: %r", router_info) - # build a request - whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) - whoisrtn.pduDestination = LocalBroadcast() + if snet_adapter is adapter: + if _debug: NetworkServiceElement._debug(" - same network") + return - # if the request had a source, forward it along - if npdu.npduSADR: - whoisrtn.npduSADR = npdu.npduSADR - else: - whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) - if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + # build a response + iamrtn = IAmRouterToNetwork([dnet], user_data=npdu.pduUserData) + iamrtn.pduDestination = npdu.pduSource - # send it to all of the (other) 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) + # send it back + self.response(adapter, iamrtn) + + else: + if _debug: NetworkServiceElement._debug(" - forwarding to other adapters") + + # build a request + whoisrtn = WhoIsRouterToNetwork(dnet, user_data=npdu.pduUserData) + whoisrtn.pduDestination = LocalBroadcast() + + # if the request had a source, forward it along + if npdu.npduSADR: + whoisrtn.npduSADR = npdu.npduSADR + else: + whoisrtn.npduSADR = RemoteStation(adapter.adapterNet, npdu.pduSource.addrAddr) + if _debug: NetworkServiceElement._debug(" - whoisrtn: %r", whoisrtn) + + # send it to all of the (other) 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) def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug("IAmRouterToNetwork %r %r", adapter, npdu) @@ -893,7 +987,7 @@ def IAmRouterToNetwork(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - sap: %r", sap) # pass along to the service access point - sap.add_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) + sap.update_router_references(adapter.adapterNet, npdu.pduSource, npdu.iartnNetworkList) # skip if this is not a router if len(sap.adapters) == 1: @@ -1091,6 +1185,9 @@ def NetworkNumberIs(self, adapter, npdu): if adapter.adapterNet is None: if _debug: NetworkServiceElement._debug(" - local network not known: %r", list(sap.adapters.keys())) + # update the routing information + sap.router_info_cache.update_source_network(None, npdu.nniNet) + # delete the reference from an unknown network del sap.adapters[None] @@ -1101,7 +1198,6 @@ def NetworkNumberIs(self, adapter, npdu): sap.adapters[adapter.adapterNet] = adapter if _debug: NetworkServiceElement._debug(" - local network learned") - ###TODO: s/None/net/g in routing tables return # check if this matches what we have @@ -1116,6 +1212,9 @@ def NetworkNumberIs(self, adapter, npdu): if _debug: NetworkServiceElement._debug(" - learning something new") + # update the routing information + sap.router_info_cache.update_source_network(adapter.adapterNet, npdu.nniNet) + # delete the reference from the old (learned) network del sap.adapters[adapter.adapterNet] @@ -1125,5 +1224,3 @@ def NetworkNumberIs(self, adapter, npdu): # we now know what network this is sap.adapters[adapter.adapterNet] = adapter - ###TODO: s/old/new/g in routing tables - diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 223d5ba2..0115050a 100644 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -384,7 +384,7 @@ def __init__(self, identifier, datatype, default=None, optional=True, mutable=Tr @bacpypes_debugging class OptionalProperty(StandardProperty): - """The property is required to be present and readable using BACnet services.""" + """The property is optional and need not be present.""" def __init__(self, identifier, datatype, default=None, optional=True, mutable=False): if _debug: @@ -462,6 +462,7 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False class Object: _debug_contents = ('_app',) + _object_supports_cov = False properties = \ [ ObjectIdentifierProperty('objectIdentifier', ObjectIdentifier, optional=False) @@ -469,6 +470,9 @@ class Object: , OptionalProperty('description', CharacterString) , OptionalProperty('profileName', CharacterString) , ReadableProperty('propertyList', ArrayOf(PropertyIdentifier)) + , OptionalProperty('tags', ArrayOf(NameValue)) + , OptionalProperty('profileLocation', CharacterString) + , OptionalProperty('profileName', CharacterString) ] _properties = {} @@ -732,6 +736,8 @@ class AccessCredentialObject(Object): @register_object_type class AccessDoorObject(Object): objectType = 'accessDoor' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', DoorValue) , ReadableProperty('statusFlags', StatusFlags) @@ -769,6 +775,8 @@ class AccessDoorObject(Object): @register_object_type class AccessPointObject(Object): objectType = 'accessPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('statusFlags', StatusFlags) , ReadableProperty('eventState', EventState) @@ -948,6 +956,8 @@ class AlertEnrollmentObject(Object): @register_object_type class AnalogInputObject(Object): objectType = 'analogInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -983,6 +993,8 @@ class AnalogInputObject(Object): @register_object_type class AnalogOutputObject(Object): objectType = 'analogOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , OptionalProperty('deviceType', CharacterString) @@ -1019,6 +1031,8 @@ class AnalogOutputObject(Object): @register_object_type class AnalogValueObject(Object): objectType = 'analogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1071,6 +1085,8 @@ class AveragingObject(Object): @register_object_type class BinaryInputObject(Object): objectType = 'binaryInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1105,6 +1121,8 @@ class BinaryInputObject(Object): @register_object_type class BinaryOutputObject(Object): objectType = 'binaryOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , OptionalProperty('deviceType', CharacterString) @@ -1143,6 +1161,8 @@ class BinaryOutputObject(Object): @register_object_type class BinaryValueObject(Object): objectType = 'binaryValue' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', BinaryPV) , ReadableProperty('statusFlags',StatusFlags) @@ -1243,6 +1263,8 @@ class ChannelObject(Object): @register_object_type class CharacterStringValueObject(Object): objectType = 'characterstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -1282,6 +1304,8 @@ class CommandObject(Object): @register_object_type class CredentialDataInputObject(Object): objectType = 'credentialDataInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', AuthenticationFactor) , ReadableProperty('statusFlags', StatusFlags) @@ -1304,6 +1328,8 @@ class CredentialDataInputObject(Object): @register_object_type class DatePatternValueObject(Object): objectType = 'datePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1317,6 +1343,8 @@ class DatePatternValueObject(Object): @register_object_type class DateValueObject(Object): objectType = 'dateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Date) , ReadableProperty('statusFlags', StatusFlags) @@ -1330,6 +1358,8 @@ class DateValueObject(Object): @register_object_type class DateTimePatternValueObject(Object): objectType = 'datetimePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1344,6 +1374,8 @@ class DateTimePatternValueObject(Object): @register_object_type class DateTimeValueObject(Object): objectType = 'datetimeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', DateTime) , ReadableProperty('statusFlags', StatusFlags) @@ -1543,6 +1575,8 @@ class GroupObject(Object): @register_object_type class IntegerValueObject(Object): objectType = 'integerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Integer) , ReadableProperty('statusFlags', StatusFlags) @@ -1578,6 +1612,8 @@ class IntegerValueObject(Object): @register_object_type class LargeAnalogValueObject(Object): objectType = 'largeAnalogValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Double) , ReadableProperty('statusFlags', StatusFlags) @@ -1613,6 +1649,8 @@ class LargeAnalogValueObject(Object): @register_object_type class LifeSafetyPointObject(Object): objectType = 'lifeSafetyPoint' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1651,6 +1689,8 @@ class LifeSafetyPointObject(Object): @register_object_type class LifeSafetyZoneObject(Object): objectType = 'lifeSafetyZone' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', LifeSafetyState) , ReadableProperty('trackingValue', LifeSafetyState) @@ -1687,6 +1727,8 @@ class LifeSafetyZoneObject(Object): @register_object_type class LightingOutputObject(Object): objectType = 'lightingOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Real) , ReadableProperty('trackingValue', Real) @@ -1717,6 +1759,8 @@ class LightingOutputObject(Object): @register_object_type class LoadControlObject(Object): objectType = 'loadControl' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', ShedState) , OptionalProperty('stateDescription', CharacterString) @@ -1751,6 +1795,8 @@ class LoadControlObject(Object): @register_object_type class LoopObject(Object): objectType = 'loop' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , ReadableProperty('statusFlags', StatusFlags) @@ -1797,6 +1843,8 @@ class LoopObject(Object): @register_object_type class MultiStateInputObject(Object): objectType = 'multiStateInput' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1826,6 +1874,8 @@ class MultiStateInputObject(Object): @register_object_type class MultiStateOutputObject(Object): objectType = 'multiStateOutput' + _object_supports_cov = True + properties = \ [ WritableProperty('presentValue', Unsigned) , OptionalProperty('deviceType', CharacterString) @@ -1856,6 +1906,8 @@ class MultiStateOutputObject(Object): @register_object_type class MultiStateValueObject(Object): objectType = 'multiStateValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -1952,10 +2004,6 @@ class NetworkPortObject(Object): , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3)) #352 , OptionalProperty('eventState', EventState) #36 , ReadableProperty('reliabilityEvaluationInhibit', Boolean) #357 - , OptionalProperty('propertyList', ArrayOf(PropertyIdentifier)) #371 - , OptionalProperty('tags', ArrayOf(NameValue)) #486 - , OptionalProperty('profileLocation', CharacterString) #91 - , OptionalProperty('profileName', CharacterString) #168 ] @register_object_type @@ -2003,6 +2051,8 @@ class NotificationForwarderObject(Object): @register_object_type class OctetStringValueObject(Object): objectType = 'octetstringValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', CharacterString) , ReadableProperty('statusFlags', StatusFlags) @@ -2016,6 +2066,8 @@ class OctetStringValueObject(Object): @register_object_type class PositiveIntegerValueObject(Object): objectType = 'positiveIntegerValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Unsigned) , ReadableProperty('statusFlags', StatusFlags) @@ -2075,6 +2127,8 @@ class ProgramObject(Object): @register_object_type class PulseConverterObject(Object): objectType = 'pulseConverter' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Real) , OptionalProperty('inputReference', ObjectPropertyReference) @@ -2149,6 +2203,8 @@ class StructuredViewObject(Object): @register_object_type class TimePatternValueObject(Object): objectType = 'timePatternValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) @@ -2162,6 +2218,8 @@ class TimePatternValueObject(Object): @register_object_type class TimeValueObject(Object): objectType = 'timeValue' + _object_supports_cov = True + properties = \ [ ReadableProperty('presentValue', Time) , ReadableProperty('statusFlags', StatusFlags) diff --git a/py34/bacpypes/pdu.py b/py34/bacpypes/pdu.py index 0454e69c..314a4ebc 100755 --- a/py34/bacpypes/pdu.py +++ b/py34/bacpypes/pdu.py @@ -13,6 +13,7 @@ except ImportError: netifaces = None +from .settings import settings from .debugging import ModuleLogger, bacpypes_debugging, btox, xtob from .comm import PCI as _PCI, PDUData @@ -28,10 +29,27 @@ # Address # -ip_address_mask_port_re = re.compile(r'^(?:(\d+):)?(\d+\.\d+\.\d+\.\d+)(?:/(\d+))?(?::(\d+))?$') +_field_address = r"((?:\d+)|(?:0x(?:[0-9A-Fa-f][0-9A-Fa-f])+))" +_ip_address_port = r"(\d+\.\d+\.\d+\.\d+)(?::(\d+))?" +_ip_address_mask_port = r"(\d+\.\d+\.\d+\.\d+)(?:/(\d+))?(?::(\d+))?" +_net_ip_address_port = r"(\d+):" + _ip_address_port +_at_route = "(?:[@](?:" + _field_address + "|" + _ip_address_port + "))?" + +field_address_re = re.compile("^" + _field_address + "$") +ip_address_port_re = re.compile("^" + _ip_address_port + "$") +ip_address_mask_port_re = re.compile("^" + _ip_address_mask_port + "$") +net_ip_address_port_re = re.compile("^" + _net_ip_address_port + "$") +net_ip_address_mask_port_re = re.compile("^" + _net_ip_address_port + "$") + ethernet_re = re.compile(r'^([0-9A-Fa-f][0-9A-Fa-f][:]){5}([0-9A-Fa-f][0-9A-Fa-f])$' ) interface_re = re.compile(r'^(?:([\w]+))(?::(\d+))?$') +net_broadcast_route_re = re.compile("^([0-9])+:[*]" + _at_route + "$") +net_station_route_re = re.compile("^([0-9])+:" + _field_address + _at_route + "$") +net_ip_address_route_re = re.compile("^([0-9])+:" + _ip_address_port + _at_route + "$") + +combined_pattern = re.compile("^(?:(?:([0-9]+)|([*])):)?(?:([*])|" + _field_address + "|" + _ip_address_mask_port + ")" + _at_route + "$") + @bacpypes_debugging class Address: nullAddr = 0 @@ -45,8 +63,9 @@ def __init__(self, *args): if _debug: Address._debug("__init__ %r", args) self.addrType = Address.nullAddr self.addrNet = None - self.addrLen = 0 - self.addrAddr = b'' + self.addrAddr = None + self.addrLen = None + self.addrRoute = None if len(args) == 1: self.decode_address(args[0]) @@ -65,25 +84,22 @@ def decode_address(self, addr): """Initialize the address from a string. Lots of different forms are supported.""" if _debug: Address._debug("decode_address %r (%s)", addr, type(addr)) - # start out assuming this is a local station + # start out assuming this is a local station and didn't get routed self.addrType = Address.localStationAddr self.addrNet = None + self.addrAddr = None + self.addrLen = None + self.addrRoute = None if addr == "*": if _debug: Address._debug(" - localBroadcast") self.addrType = Address.localBroadcastAddr - self.addrNet = None - self.addrAddr = None - self.addrLen = None elif addr == "*:*": if _debug: Address._debug(" - globalBroadcast") self.addrType = Address.globalBroadcastAddr - self.addrNet = None - self.addrAddr = None - self.addrLen = None elif isinstance(addr, int): if _debug: Address._debug(" - int") @@ -112,44 +128,100 @@ def decode_address(self, addr): elif isinstance(addr, str): if _debug: Address._debug(" - str") - m = ip_address_mask_port_re.match(addr) + m = combined_pattern.match(addr) if m: - if _debug: Address._debug(" - IP address") + if _debug: Address._debug(" - combined pattern") + + (net, global_broadcast, + local_broadcast, + local_addr, + local_ip_addr, local_ip_net, local_ip_port, + route_addr, route_ip_addr, route_ip_port + ) = m.groups() + + if global_broadcast and local_broadcast: + if _debug: Address._debug(" - global broadcast") + self.addrType = Address.globalBroadcastAddr + + elif net and local_broadcast: + if _debug: Address._debug(" - remote broadcast") + net_addr = int(net) + if (net_addr >= 65535): + raise ValueError("network out of range") + self.addrType = Address.remoteBroadcastAddr + self.addrNet = net_addr - net, addr, mask, port = m.groups() - if not mask: mask = '32' - if not port: port = '47808' - if _debug: Address._debug(" - net, addr, mask, port: %r, %r, %r, %r", net, addr, mask, port) + elif local_broadcast: + if _debug: Address._debug(" - local broadcast") + self.addrType = Address.localBroadcastAddr - if net: - net = int(net) - if (net >= 65535): + elif net: + if _debug: Address._debug(" - remote station") + net_addr = int(net) + if (net_addr >= 65535): raise ValueError("network out of range") self.addrType = Address.remoteStationAddr - self.addrNet = net - - self.addrPort = int(port) - self.addrTuple = (addr, self.addrPort) - - addrstr = socket.inet_aton(addr) - self.addrIP = struct.unpack('!L', addrstr)[0] - self.addrMask = (_long_mask << (32 - int(mask))) & _long_mask - self.addrHost = (self.addrIP & ~self.addrMask) - self.addrSubnet = (self.addrIP & self.addrMask) - - bcast = (self.addrSubnet | ~self.addrMask) - self.addrBroadcastTuple = (socket.inet_ntoa(struct.pack('!L', bcast & _long_mask)), self.addrPort) - - self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) - self.addrLen = 6 - - elif ethernet_re.match(addr): + self.addrNet = net_addr + + if local_addr: + if _debug: Address._debug(" - simple address") + if local_addr.startswith("0x"): + self.addrAddr = xtob(local_addr[2:]) + self.addrLen = len(self.addrAddr) + else: + local_addr = int(local_addr) + if local_addr >= 256: + raise ValueError("address out of range") + + self.addrAddr = struct.pack('B', local_addr) + self.addrLen = 1 + + if local_ip_addr: + if _debug: Address._debug(" - ip address") + if not local_ip_port: + local_ip_port = '47808' + if not local_ip_net: + local_ip_net = '32' + + self.addrPort = int(local_ip_port) + self.addrTuple = (local_ip_addr, self.addrPort) + if _debug: Address._debug(" - addrTuple: %r", self.addrTuple) + + addrstr = socket.inet_aton(local_ip_addr) + self.addrIP = struct.unpack('!L', addrstr)[0] + self.addrMask = (_long_mask << (32 - int(local_ip_net))) & _long_mask + self.addrHost = (self.addrIP & ~self.addrMask) + self.addrSubnet = (self.addrIP & self.addrMask) + + bcast = (self.addrSubnet | ~self.addrMask) + self.addrBroadcastTuple = (socket.inet_ntoa(struct.pack('!L', bcast & _long_mask)), self.addrPort) + if _debug: Address._debug(" - addrBroadcastTuple: %r", self.addrBroadcastTuple) + + self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) + self.addrLen = 6 + + if (not settings.route_aware) and (route_addr or route_ip_addr): + Address._warning("route provided but not route aware: %r", addr) + + if route_addr: + self.addrRoute = Address(int(route_addr)) + if _debug: Address._debug(" - addrRoute: %r", self.addrRoute) + elif route_ip_addr: + if not route_ip_port: + route_ip_port = '47808' + self.addrRoute = Address((route_ip_addr, int(route_ip_port))) + if _debug: Address._debug(" - addrRoute: %r", self.addrRoute) + + return + + if ethernet_re.match(addr): if _debug: Address._debug(" - ethernet") self.addrAddr = xtob(addr, ':') self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+$", addr): + if re.match(r"^\d+$", addr): if _debug: Address._debug(" - int") addr = int(addr) @@ -158,8 +230,9 @@ def decode_address(self, addr): self.addrAddr = struct.pack('B', addr) self.addrLen = 1 + return - elif re.match(r"^\d+:[*]$", addr): + if re.match(r"^\d+:[*]$", addr): if _debug: Address._debug(" - remote broadcast") addr = int(addr[:-2]) @@ -170,8 +243,9 @@ def decode_address(self, addr): self.addrNet = addr self.addrAddr = None self.addrLen = None + return - elif re.match(r"^\d+:\d+$",addr): + if re.match(r"^\d+:\d+$",addr): if _debug: Address._debug(" - remote station") net, addr = addr.split(':') @@ -186,20 +260,23 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = struct.pack('B', addr) self.addrLen = 1 + return - elif re.match(r"^0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): + if re.match(r"^0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): if _debug: Address._debug(" - modern hex string") self.addrAddr = xtob(addr[2:]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): + if re.match(r"^X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): if _debug: Address._debug(" - old school hex string") self.addrAddr = xtob(addr[2:-1]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+:0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): + if re.match(r"^\d+:0x([0-9A-Fa-f][0-9A-Fa-f])+$",addr): if _debug: Address._debug(" - remote station with modern hex string") net, addr = addr.split(':') @@ -211,8 +288,9 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = xtob(addr[2:]) self.addrLen = len(self.addrAddr) + return - elif re.match(r"^\d+:X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): + if re.match(r"^\d+:X'([0-9A-Fa-f][0-9A-Fa-f])+'$",addr): if _debug: Address._debug(" - remote station with old school hex string") net, addr = addr.split(':') @@ -224,8 +302,9 @@ def decode_address(self, addr): self.addrNet = net self.addrAddr = xtob(addr[2:-1]) self.addrLen = len(self.addrAddr) + return - elif netifaces and interface_re.match(addr): + if netifaces and interface_re.match(addr): if _debug: Address._debug(" - interface name with optional port") interface, port = interface_re.match(addr).groups() @@ -273,9 +352,9 @@ def decode_address(self, addr): self.addrAddr = addrstr + struct.pack('!H', self.addrPort & _short_mask) self.addrLen = 6 + return - else: - raise ValueError("unrecognized format") + raise ValueError("unrecognized format") elif isinstance(addr, tuple): addr, port = addr @@ -312,10 +391,10 @@ def decode_address(self, addr): def __str__(self): if self.addrType == Address.nullAddr: - return 'Null' + rslt = 'Null' elif self.addrType == Address.localBroadcastAddr: - return '*' + rslt = '*' elif self.addrType == Address.localStationAddr: rslt = '' @@ -329,10 +408,9 @@ def __str__(self): rslt += ':' + str(port) else: rslt += '0x' + btox(self.addrAddr) - return rslt elif self.addrType == Address.remoteBroadcastAddr: - return '%d:*' % (self.addrNet,) + rslt = '%d:*' % (self.addrNet,) elif self.addrType == Address.remoteStationAddr: rslt = '%d:' % (self.addrNet,) @@ -346,31 +424,52 @@ def __str__(self): rslt += ':' + str(port) else: rslt += '0x' + btox(self.addrAddr) - return rslt elif self.addrType == Address.globalBroadcastAddr: - return '*:*' + rslt = "*:*" else: raise TypeError("unknown address type %d" % self.addrType) + if self.addrRoute: + rslt += "@" + str(self.addrRoute) + + return rslt + def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.__str__()) + def _tuple(self): + if (not settings.route_aware) or (self.addrRoute is None): + return (self.addrType, self.addrNet, self.addrAddr, None) + else: + return (self.addrType, self.addrNet, self.addrAddr, self.addrRoute._tuple()) + def __hash__(self): - return hash( (self.addrType, self.addrNet, self.addrAddr) ) + return hash(self._tuple()) - def __eq__(self,arg): + def __eq__(self, arg): # try an coerce it into an address if not isinstance(arg, Address): arg = Address(arg) - # all of the components must match - return (self.addrType == arg.addrType) and (self.addrNet == arg.addrNet) and (self.addrAddr == arg.addrAddr) + # basic components must match + rslt = (self.addrType == arg.addrType) + rslt = rslt and (self.addrNet == arg.addrNet) + rslt = rslt and (self.addrAddr == arg.addrAddr) - def __ne__(self,arg): + # if both have routes they must match + if rslt and self.addrRoute and arg.addrRoute: + rslt = rslt and (self.addrRoute == arg.addrRoute) + + return rslt + + def __ne__(self, arg): return not self.__eq__(arg) + def __lt__(self, arg): + return self._tuple() < arg._tuple() + def dict_contents(self, use_dict=None, as_class=None): """Return the contents of an object as a dict.""" if _debug: _log.debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) @@ -400,9 +499,10 @@ def unpack_ip_addr(addr): class LocalStation(Address): - def __init__(self, addr): + def __init__(self, addr, route=None): self.addrType = Address.localStationAddr self.addrNet = None + self.addrRoute = route if isinstance(addr, int): if (addr < 0) or (addr >= 256): @@ -426,7 +526,7 @@ def __init__(self, addr): class RemoteStation(Address): - def __init__(self, net, addr): + def __init__(self, net, addr, route=None): if not isinstance(net, int): raise TypeError("integer network required") if (net < 0) or (net >= 65535): @@ -434,6 +534,7 @@ def __init__(self, net, addr): self.addrType = Address.remoteStationAddr self.addrNet = net + self.addrRoute = route if isinstance(addr, int): if (addr < 0) or (addr >= 256): @@ -457,11 +558,12 @@ def __init__(self, net, addr): class LocalBroadcast(Address): - def __init__(self): + def __init__(self, route=None): self.addrType = Address.localBroadcastAddr self.addrNet = None self.addrAddr = None self.addrLen = None + self.addrRoute = route # # RemoteBroadcast @@ -469,7 +571,7 @@ def __init__(self): class RemoteBroadcast(Address): - def __init__(self, net): + def __init__(self, net, route=None): if not isinstance(net, int): raise TypeError("integer network required") if (net < 0) or (net >= 65535): @@ -479,6 +581,7 @@ def __init__(self, net): self.addrNet = net self.addrAddr = None self.addrLen = None + self.addrRoute = route # # GlobalBroadcast @@ -486,11 +589,12 @@ def __init__(self, net): class GlobalBroadcast(Address): - def __init__(self): + def __init__(self, route=None): self.addrType = Address.globalBroadcastAddr self.addrNet = None self.addrAddr = None self.addrLen = None + self.addrRoute = route # # PCI diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index d3b341ca..2d9557be 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -612,6 +612,14 @@ def __init__(self, arg=None): if not self.is_valid(arg): raise ValueError("value out of range") self.value = arg + elif isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + raise TypeError("invalid constructor datatype") + if not self.is_valid(arg): + raise ValueError("value out of range") + self.value = arg elif isinstance(arg, Unsigned): if not self.is_valid(arg.value): raise ValueError("value out of range") @@ -647,7 +655,12 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - if not isinstance(arg, int) or isinstance(arg, bool): + if isinstance(arg, str): + try: + arg = int(arg) + except ValueError: + return False + elif not isinstance(arg, int) or isinstance(arg, bool): return False if (arg < cls._low_limit): return False @@ -915,7 +928,15 @@ def decode(self, tag): # normalize the value if (self.strEncoding == 0): - self.value = self.strValue.decode('utf-8') + try: + self.value = self.strValue.decode("utf-8", "strict") + except UnicodeDecodeError: + # Wrong encoding... trying with latin-1 as + # we probably face a Windows software encoding issue + try: + self.value = self.strValue.decode("latin-1") + except UnicodeDecodeError: + raise elif (self.strEncoding == 3): self.value = self.strValue.decode('utf_32be') elif (self.strEncoding == 4): @@ -1644,6 +1665,7 @@ class ObjectType(Enumerated): , 'timeValue':50 , 'trendLog':20 , 'trendLogMultiple':27 + , 'networkPort':56 } expand_enumerations(ObjectType) diff --git a/py34/bacpypes/service/cov.py b/py34/bacpypes/service/cov.py index 21fd0b60..3ba68158 100644 --- a/py34/bacpypes/service/cov.py +++ b/py34/bacpypes/service/cov.py @@ -471,6 +471,7 @@ def send_cov_notifications(self, subscription=None): # mapping from object type to appropriate criteria class criteria_type_map = { +# 'accessDoor': GenericCriteria, #TODO: needs AccessDoorCriteria 'accessPoint': AccessPointCriteria, 'analogInput': COVIncrementCriteria, 'analogOutput': COVIncrementCriteria, @@ -497,6 +498,7 @@ def send_cov_notifications(self, subscription=None): 'dateTimePatternValue': GenericCriteria, 'credentialDataInput': CredentialDataInputCriteria, 'loadControl': LoadControlCriteria, + 'loop': GenericCriteria, 'pulseConverter': PulseConverterCriteria, } @@ -692,6 +694,10 @@ def do_SubscribeCOVRequest(self, apdu): if not obj: raise ExecutionError(errorClass='object', errorCode='unknownObject') + # check to see if the object supports COV + if not obj._object_supports_cov: + raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed') + # look for an algorithm already associated with this object cov_detection = self.cov_detections.get(obj, None) diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 11be52b0..f1095109 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -25,6 +25,12 @@ def __init__(self): if _debug: WhoIsIAmServices._debug("__init__") Capability.__init__(self) + def startup(self): + if _debug: WhoIsIAmServices._debug("startup") + + # send a global broadcast I-Am + self.i_am() + def who_is(self, low_limit=None, high_limit=None, address=None): if _debug: WhoIsIAmServices._debug("who_is") diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index 3289075d..f6ec6fd0 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -5,7 +5,7 @@ from ..basetypes import ErrorType, PropertyIdentifier from ..primitivedata import Atomic, Null, Unsigned -from ..constructeddata import Any, Array, ArrayOf +from ..constructeddata import Any, Array, ArrayOf, List from ..apdu import Error, \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ @@ -72,6 +72,8 @@ def do_ReadPropertyRequest(self, apdu): elif not isinstance(value, datatype.subtype): raise TypeError("invalid result datatype, expecting {0} and got {1}" \ .format(datatype.subtype.__name__, type(value).__name__)) + elif issubclass(datatype, List): + value = datatype(value) elif not isinstance(value, datatype): raise TypeError("invalid result datatype, expecting {0} and got {1}" \ .format(datatype.__name__, type(value).__name__)) @@ -289,6 +291,11 @@ def do_ReadPropertyMultipleRequest(self, apdu): for propId, prop in obj._properties.items(): if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + # skip propertyList for ReadPropertyMultiple + if (propId == 'propertyList'): + if _debug: ReadWritePropertyMultipleServices._debug(" - ignore propertyList") + continue + if (propertyIdentifier == 'all'): pass elif (propertyIdentifier == 'required') and (prop.optional): diff --git a/py34/bacpypes/settings.py b/py34/bacpypes/settings.py new file mode 100644 index 00000000..ba89b099 --- /dev/null +++ b/py34/bacpypes/settings.py @@ -0,0 +1,95 @@ +#!/usr/bin/python + +""" +Settings +""" + +import os + + +class Settings(dict): + """ + Settings + """ + + def __getattr__(self, name): + if name not in self: + raise AttributeError("No such setting: " + name) + return self[name] + + def __setattr__(self, name, value): + if name not in self: + raise AttributeError("No such setting: " + name) + self[name] = value + + +# globals +settings = Settings( + debug=set(), + color=False, + debug_file="", + max_bytes=1048576, + backup_count=5, + route_aware=False, +) + + +def os_settings(): + """ + Update the settings from known OS environment variables. + """ + for setting_name, env_name in ( + ("debug", "BACPYPES_DEBUG"), + ("color", "BACPYPES_COLOR"), + ("debug_file", "BACPYPES_DEBUG_FILE"), + ("max_bytes", "BACPYPES_MAX_BYTES"), + ("backup_count", "BACPYPES_BACKUP_COUNT"), + ("route_aware", "BACPYPES_ROUTE_AWARE"), + ): + env_value = os.getenv(env_name, None) + if env_value is not None: + cur_value = settings[setting_name] + + if isinstance(cur_value, bool): + env_value = env_value.lower() + if env_value in ("set", "true"): + env_value = True + elif env_value in ("reset", "false"): + env_value = False + else: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, int): + try: + env_value = int(env_value) + except: + raise ValueError("setting: " + setting_name) + elif isinstance(cur_value, str): + pass + elif isinstance(cur_value, list): + env_value = env_value.split() + elif isinstance(cur_value, set): + env_value = set(env_value.split()) + else: + raise TypeError("setting type: " + setting_name) + settings[setting_name] = env_value + + +def dict_settings(**kwargs): + """ + Update the settings from key/value content. Lists are morphed into sets + if necessary, giving a setting any value is acceptable if there isn't one + already set, otherwise protect against setting type changes. + """ + for setting_name, kw_value in kwargs.items(): + cur_value = settings.get(setting_name, None) + + if cur_value is None: + pass + elif isinstance(cur_value, set): + if isinstance(kw_value, list): + kw_value = set(kw_value) + elif not isinstance(kw_value, set): + raise TypeError(setting_name) + elif not isinstance(kw_value, type(cur_value)): + raise TypeError("setting type: " + setting_name) + settings[setting_name] = kw_value diff --git a/py34/bacpypes/udp.py b/py34/bacpypes/udp.py index 47ede293..c6aaabb9 100755 --- a/py34/bacpypes/udp.py +++ b/py34/bacpypes/udp.py @@ -151,7 +151,12 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non self.set_reuse_addr() # proceed with the bind - self.bind(address) + try: + self.bind(address) + except socket.error as err: + if _debug: UDPDirector._debug(" - bind error: %r", err) + self.close() + raise if _debug: UDPDirector._debug(" - getsockname: %r", self.socket.getsockname()) # allow it to send broadcasts @@ -200,7 +205,7 @@ def readable(self): return 1 def handle_read(self): - if _debug: UDPDirector._debug("handle_read") + if _debug: UDPDirector._debug("handle_read(%r)", self.address) try: msg, addr = self.socket.recvfrom(65536) @@ -227,7 +232,7 @@ def writable(self): def handle_write(self): """get a PDU from the queue and send it.""" - if _debug: UDPDirector._debug("handle_write") + if _debug: UDPDirector._debug("handle_write(%r)", self.address) try: pdu = self.request.get() diff --git a/release_to_pypi.sh b/release_to_pypi.sh index 95c9246a..ccd4487f 100755 --- a/release_to_pypi.sh +++ b/release_to_pypi.sh @@ -10,7 +10,7 @@ rm -Rfv build/ # python2.5 setup.py bdist_egg # rm -Rfv build/ -for version in 2.7 3.4 3.5 3.6; do +for version in 2.7 3.4 3.5 3.6 3.7; do if [ -a "`which python$version`" ]; then python$version setup.py bdist_egg python$version setup.py bdist_wheel diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index 9e274e89..84a499a3 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -116,7 +116,7 @@ def __init__(self, vlan_device, vlan_address, aseID=None): # create a vlan node at the assigned address self.vlan_node = Node(vlan_address) - # bind the stack to the node, no network number + # bind the stack to the node, no network number, no address self.nsap.bind(self.vlan_node) def request(self, apdu): @@ -213,11 +213,12 @@ def main(): vlan = Network(broadcast_address=LocalBroadcast()) # create a node for the router, address 1 on the VLAN - router_node = Node(Address(1)) + router_addr = Address(1) + router_node = Node(router_addr) vlan.add_node(router_node) # bind the router stack to the vlan network through this node - router.nsap.bind(router_node, vlan_network) + router.nsap.bind(router_node, vlan_network, router_addr) # send network topology deferred(router.nse.i_am_router_to_network) diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 8e9a85f0..e9582878 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -4,417 +4,85 @@ Rebuilt Commandable """ -from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.debugging import ModuleLogger from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.core import run -from bacpypes.task import OneShotTask -from bacpypes.errors import ExecutionError -from bacpypes.primitivedata import BitString, CharacterString, Date, Integer, \ - Double, Enumerated, OctetString, Real, Time, Unsigned -from bacpypes.basetypes import BinaryPV, ChannelValue, DateTime, DoorValue, PriorityValue, \ - PriorityArray -from bacpypes.object import Property, ReadableProperty, WritableProperty, \ - register_object_type, \ - AccessDoorObject, AnalogOutputObject, AnalogValueObject, \ - BinaryOutputObject, BinaryValueObject, BitStringValueObject, CharacterStringValueObject, \ - DateValueObject, DatePatternValueObject, DateTimePatternValueObject, \ - DateTimeValueObject, IntegerValueObject, \ - LargeAnalogValueObject, LightingOutputObject, MultiStateOutputObject, \ - MultiStateValueObject, OctetStringValueObject, PositiveIntegerValueObject, \ - TimeValueObject, TimePatternValueObject, ChannelObject +from bacpypes.primitivedata import Date from bacpypes.app import BIPSimpleApplication -from bacpypes.local.object import CurrentPropertyListMixIn -from bacpypes.local.device import LocalDeviceObject +from bacpypes.object import register_object_type +from bacpypes.local.device import ( + LocalDeviceObject, +) +from bacpypes.local.object import ( + AnalogValueCmdObject, + BinaryOutputCmdObject, + DateValueCmdObject, +) # some debugging _debug = 0 _log = ModuleLogger(globals()) - -# -# Commandable -# - -@bacpypes_debugging -def Commandable(datatype, presentValue='presentValue', priorityArray='priorityArray', relinquishDefault='relinquishDefault'): - if _debug: Commandable._debug("Commandable %r ...", datatype) - - class _Commando(object): - - properties = [ - WritableProperty(presentValue, datatype), - ReadableProperty(priorityArray, PriorityArray), - ReadableProperty(relinquishDefault, datatype), - ] - - _pv_choice = None - - def __init__(self, **kwargs): - super(_Commando, self).__init__(**kwargs) - - # build a default value in case one is needed - default_value = datatype().value - if issubclass(datatype, Enumerated): - default_value = datatype._xlate_table[default_value] - if _debug: Commandable._debug(" - default_value: %r", default_value) - - # see if a present value was provided - if (presentValue not in kwargs): - setattr(self, presentValue, default_value) - - # see if a priority array was provided - if (priorityArray not in kwargs): - setattr(self, priorityArray, PriorityArray()) - - # see if a present value was provided - if (relinquishDefault not in kwargs): - setattr(self, relinquishDefault, default_value) - - def _highest_priority_value(self): - if _debug: Commandable._debug("_highest_priority_value") - - priority_array = getattr(self, priorityArray) - for i in range(1, 17): - priority_value = priority_array[i] - if priority_value.null is None: - if _debug: Commandable._debug(" - found at index: %r", i) - - value = getattr(priority_value, _Commando._pv_choice) - value_source = "###" - - if issubclass(datatype, Enumerated): - value = datatype._xlate_table[value] - if _debug: Commandable._debug(" - remapped enumeration: %r", value) - - break - else: - value = getattr(self, relinquishDefault) - value_source = None - - if _debug: Commandable._debug(" - value, value_source: %r, %r", value, value_source) - - # return what you found - return value, value_source - - def WriteProperty(self, property, value, arrayIndex=None, priority=None, direct=False): - if _debug: Commandable._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", property, value, arrayIndex, priority, direct) - - # when writing to the presentValue with a priority - if (property == presentValue): - if _debug: Commandable._debug(" - writing to %s, priority %r", presentValue, priority) - - # default (lowest) priority - if priority is None: - priority = 16 - if _debug: Commandable._debug(" - translate to priority array, index %d", priority) - - # translate to updating the priority array - property = priorityArray - arrayIndex = priority - priority = None - - # update the priority array entry - if (property == priorityArray): - if (arrayIndex is None): - if _debug: Commandable._debug(" - writing entire %s", priorityArray) - - # pass along the request - super(_Commando, self).WriteProperty( - property, value, - arrayIndex=arrayIndex, priority=priority, direct=direct, - ) - else: - if _debug: Commandable._debug(" - writing to %s, array index %d", priorityArray, arrayIndex) - - # check the bounds - if arrayIndex == 0: - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - if (arrayIndex < 1) or (arrayIndex > 16): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - - # update the specific priorty value element - priority_value = getattr(self, priorityArray)[arrayIndex] - if _debug: Commandable._debug(" - priority_value: %r", priority_value) - - # the null or the choice has to be set, the other clear - if value is (): - if _debug: Commandable._debug(" - write a null") - priority_value.null = value - setattr(priority_value, _Commando._pv_choice, None) - else: - if _debug: Commandable._debug(" - write a value") - - if issubclass(datatype, Enumerated): - value = datatype._xlate_table[value] - if _debug: Commandable._debug(" - remapped enumeration: %r", value) - - priority_value.null = None - setattr(priority_value, _Commando._pv_choice, value) - - # look for the highest priority value - value, value_source = self._highest_priority_value() - - # compare with the current value - current_value = getattr(self, presentValue) - if value == current_value: - if _debug: Commandable._debug(" - no present value change") - return - - # turn this into a present value change - property = presentValue - arrayIndex = priority = None - - # allow the request to pass through - if _debug: Commandable._debug(" - super: %r %r arrayIndex=%r priority=%r", property, value, arrayIndex, priority) - - super(_Commando, self).WriteProperty( - property, value, - arrayIndex=arrayIndex, priority=priority, direct=direct, - ) - - # look up a matching priority value choice - for element in PriorityValue.choiceElements: - if issubclass(datatype, element.klass): - _Commando._pv_choice = element.name - break - else: - _Commando._pv_choice = 'constructedValue' - if _debug: Commandable._debug(" - _pv_choice: %r", _Commando._pv_choice) - - # return the class - return _Commando - -# -# MinOnOffTask -# - -@bacpypes_debugging -class MinOnOffTask(OneShotTask): - - def __init__(self, binary_obj): - if _debug: MinOnOffTask._debug("__init__ %s", repr(binary_obj)) - OneShotTask.__init__(self) - - # save a reference to the object - self.binary_obj = binary_obj - - # listen for changes to the present value - self.binary_obj._property_monitors['presentValue'].append(self.present_value_change) - - def present_value_change(self, old_value, new_value): - if _debug: MinOnOffTask._debug("present_value_change %r %r", old_value, new_value) - - # if there's no value change, skip all this - if old_value == new_value: - if _debug: MinOnOffTask._debug(" - no state change") - return - - # get the minimum on/off time - if new_value == 'inactive': - task_delay = getattr(self.binary_obj, 'minimumOnTime') or 0 - if _debug: MinOnOffTask._debug(" - minimum on: %r", task_delay) - elif new_value == 'active': - task_delay = getattr(self.binary_obj, 'minimumOffTime') or 0 - if _debug: MinOnOffTask._debug(" - minimum off: %r", task_delay) - else: - raise ValueError("unrecognized present value for %r: %r" % (self.binary_obj.objectIdentifier, new_value)) - - # if there's no delay, don't bother - if not task_delay: - if _debug: MinOnOffTask._debug(" - no delay") - return - - # set the value at priority 6 - self.binary_obj.WriteProperty('presentValue', new_value, priority=6) - - # install this to run, if there is a delay - self.install_task(delta=task_delay) - - def process_task(self): - if _debug: MinOnOffTask._debug("process_task(%s)", self.binary_obj.objectName) - - # clear the value at priority 6 - self.binary_obj.WriteProperty('presentValue', (), priority=6) - -# -# MinOnOff -# - -@bacpypes_debugging -class MinOnOff(object): - - def __init__(self, **kwargs): - if _debug: MinOnOff._debug("__init__ ...") - super(MinOnOff, self).__init__(**kwargs) - - # create the timer task - self._min_on_off_task = MinOnOffTask(self) - -# -# Commandable Standard Objects -# - -class AccessDoorObjectCmd(Commandable(DoorValue), AccessDoorObject): - pass - -class AnalogOutputObjectCmd(Commandable(Real), AnalogOutputObject): - pass - -class AnalogValueObjectCmd(Commandable(Real), AnalogValueObject): - pass - -### class BinaryLightingOutputObjectCmd(Commandable(Real), BinaryLightingOutputObject): -### pass - -class BinaryOutputObjectCmd(Commandable(BinaryPV), MinOnOff, BinaryOutputObject): - pass - -class BinaryValueObjectCmd(Commandable(BinaryPV), MinOnOff, BinaryValueObject): - pass - -class BitStringValueObjectCmd(Commandable(BitString), BitStringValueObject): - pass - -class CharacterStringValueObjectCmd(Commandable(CharacterString), CharacterStringValueObject): - pass - -class DateValueObjectCmd(Commandable(Date), DateValueObject): - pass - -class DatePatternValueObjectCmd(Commandable(Date), DatePatternValueObject): - pass - -class DateTimeValueObjectCmd(Commandable(DateTime), DateTimeValueObject): - pass - -class DateTimePatternValueObjectCmd(Commandable(DateTime), DateTimePatternValueObject): - pass - -class IntegerValueObjectCmd(Commandable(Integer), IntegerValueObject): - pass - -class LargeAnalogValueObjectCmd(Commandable(Double), LargeAnalogValueObject): - pass - -class LightingOutputObjectCmd(Commandable(Real), LightingOutputObject): - pass - -class MultiStateOutputObjectCmd(Commandable(Unsigned), MultiStateOutputObject): - pass - -class MultiStateValueObjectCmd(Commandable(Unsigned), MultiStateValueObject): - pass - -class OctetStringValueObjectCmd(Commandable(OctetString), OctetStringValueObject): - pass - -class PositiveIntegerValueObjectCmd(Commandable(Unsigned), PositiveIntegerValueObject): - pass - -class TimeValueObjectCmd(Commandable(Time), TimeValueObject): - pass - -class TimePatternValueObjectCmd(Commandable(Time), TimePatternValueObject): - pass - -# -# ChannelValueProperty -# - -class ChannelValueProperty(Property): - - def __init__(self): - if _debug: ChannelValueProperty._debug("__init__") - Property.__init__(self, 'presentValue', ChannelValue, default=None, optional=False, mutable=True) - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - if _debug: ChannelValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) - - ### Clause 12.53.5, page 487 - raise NotImplementedError() - -# -# ChannelObjectCmd -# - -class ChannelObjectCmd(ChannelObject): - - properties = [ - ChannelValueProperty(), - ] - -## -## -## -## -## - -@register_object_type(vendor_id=999) -class LocalAnalogValueObjectCmd(CurrentPropertyListMixIn, AnalogValueObjectCmd): - pass - -@register_object_type(vendor_id=999) -class LocalBinaryOutputObjectCmd(CurrentPropertyListMixIn, BinaryOutputObjectCmd): - pass - -@register_object_type(vendor_id=999) -class LocalDateValueObjectCmd(CurrentPropertyListMixIn, DateValueObjectCmd): - pass - -# -# __main__ -# +# register the classes +register_object_type(LocalDeviceObject, vendor_id=999) +register_object_type(AnalogValueCmdObject, vendor_id=999) +register_object_type(BinaryOutputCmdObject, vendor_id=999) +register_object_type(DateValueCmdObject, vendor_id=999) def main(): + global this_application + # parse the command line arguments args = ConfigArgumentParser(description=__doc__).parse_args() - if _debug: _log.debug("initialization") - if _debug: _log.debug(" - args: %r", args) + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) # make a device object this_device = LocalDeviceObject(ini=args.ini) - if _debug: _log.debug(" - this_device: %r", this_device) + if _debug: + _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) # make a commandable analog value object, add to the device - avo1 = LocalAnalogValueObjectCmd( - objectIdentifier=('analogValue', 1), - objectName='avo1', - ) - if _debug: _log.debug(" - avo1: %r", avo1) + avo1 = AnalogValueCmdObject(objectIdentifier=("analogValue", 1), objectName="avo1") + if _debug: + _log.debug(" - avo1: %r", avo1) this_application.add_object(avo1) # make a commandable binary output object, add to the device - boo1 = LocalBinaryOutputObjectCmd( - objectIdentifier=('binaryOutput', 1), - objectName='boo1', - presentValue='inactive', - relinquishDefault='inactive', - minimumOnTime=5, # let it warm up - minimumOffTime=10, # let it cool off - ) - if _debug: _log.debug(" - boo1: %r", boo1) + boo1 = BinaryOutputCmdObject( + objectIdentifier=("binaryOutput", 1), + objectName="boo1", + presentValue="inactive", + relinquishDefault="inactive", + minimumOnTime=5, # let it warm up + minimumOffTime=10, # let it cool off + ) + if _debug: + _log.debug(" - boo1: %r", boo1) this_application.add_object(boo1) # get the current date today = Date().now() # make a commandable date value object, add to the device - dvo1 = LocalDateValueObjectCmd( - objectIdentifier=('dateValue', 1), - objectName='dvo1', - presentValue=today.value, - ) - if _debug: _log.debug(" - dvo1: %r", dvo1) + dvo1 = DateValueCmdObject( + objectIdentifier=("dateValue", 1), objectName="dvo1", presentValue=today.value + ) + if _debug: + _log.debug(" - dvo1: %r", dvo1) this_application.add_object(dvo1) - if _debug: _log.debug("running") + if _debug: + _log.debug("running") run() diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index dd5b6061..9b232bad 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -102,7 +102,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index 09c641b0..9a134d6c 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -176,7 +176,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index 53493f42..f5df8e24 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -176,7 +176,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/EventNotifications.py b/samples/EventNotifications.py index de5309ed..fb6da899 100755 --- a/samples/EventNotifications.py +++ b/samples/EventNotifications.py @@ -221,7 +221,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/IP2IPRouter.py b/samples/IP2IPRouter.py index 517040b2..8cb7d4a2 100755 --- a/samples/IP2IPRouter.py +++ b/samples/IP2IPRouter.py @@ -75,7 +75,7 @@ def __init__(self, addr1, net1, addr2, net2): bind(self.s2_bip, self.s2_annexj, self.s2_mux.annexJ) # bind the BIP stack to the local network - self.nsap.bind(self.s2_bip, net2) + self.nsap.bind(self.s2_bip, net2, addr2) # # __main__ diff --git a/samples/IP2VLANRouter.py b/samples/IP2VLANRouter.py index da58c118..6cfd2580 100755 --- a/samples/IP2VLANRouter.py +++ b/samples/IP2VLANRouter.py @@ -150,7 +150,7 @@ def __init__(self, vlan_device, vlan_address, aseID=None): # create a vlan node at the assigned address self.vlan_node = Node(vlan_address) - # bind the stack to the node, no network number + # bind the stack to the node, no network number, no addresss self.nsap.bind(self.vlan_node) def request(self, apdu): @@ -261,11 +261,12 @@ def main(): vlan = Network(broadcast_address=LocalBroadcast()) # create a node for the router, address 1 on the VLAN - router_node = Node(Address(1)) + router_addr = Address(1) + router_node = Node(router_addr) vlan.add_node(router_node) # bind the router stack to the vlan network through this node - router.nsap.bind(router_node, vlan_network) + router.nsap.bind(router_node, vlan_network, router_addr) # send network topology deferred(router.nse.i_am_router_to_network) diff --git a/samples/NATRouter.py b/samples/NATRouter.py index c7b19d70..af31135c 100755 --- a/samples/NATRouter.py +++ b/samples/NATRouter.py @@ -61,7 +61,7 @@ def __init__(self, addr1, port1, net1, addr2, port2, net2): bind(self.s1_bip, self.s1_annexj, self.s1_mux.annexJ) # bind the BIP stack to the local network - self.nsap.bind(self.s1_bip, net1, addr1) + self.nsap.bind(self.s1_bip, net1, local_addr) #== Second stack @@ -78,7 +78,7 @@ def __init__(self, addr1, port1, net1, addr2, port2, net2): bind(self.s2_bip, self.s2_annexj, self.s2_mux.annexJ) # bind the BIP stack to the global network - self.nsap.bind(self.s2_bip, net2) + self.nsap.bind(self.s2_bip, net2, global_addr) # # __main__ diff --git a/samples/OpenWeatherServer.py b/samples/OpenWeatherServer.py new file mode 100644 index 00000000..6183c0d0 --- /dev/null +++ b/samples/OpenWeatherServer.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- + +""" +OpenWeather Server + +This sample application uses the https://openweathermap.org/ service to get +weather data and make it available over BACnet. First sign up for an API +key called APPID and set an environment variable to that value. You can also +change the units and update interval. +""" + +import os +import requests +import time + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.task import recurring_function + +from bacpypes.basetypes import DateTime +from bacpypes.object import AnalogValueObject, DateTimeValueObject + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# application ID is the API key from the service +APPID = os.getenv("APPID") + +# units go into the request +APPUNITS = os.getenv("APPUNITS", "metric") + +# application interval is refresh time in minutes (default 5) +APPINTERVAL = int(os.getenv("APPINTERVAL", 5)) * 60 * 1000 + +# request parameters are passed to the API, id is the city identifier +request_parameters = {"id": 7258390, "APPID": APPID, "units": APPUNITS} + +# dictionary of names to objects +objects = {} + + +@bacpypes_debugging +class LocalAnalogValueObject(AnalogValueObject): + def _set_value(self, value): + if _debug: + LocalAnalogValueObject._debug("_set_value %r", value) + + # numeric values are easy to set + self.presentValue = value + + +# timezone offset is shared with the date time values +timezone_offset = 0 + + +@bacpypes_debugging +class LocalDateTimeValueObject(DateTimeValueObject): + def _set_value(self, utc_time): + if _debug: + LocalDateTimeValueObject._debug("_set_value %r", utc_time) + + # convert to a time tuple based on timezone offset + time_tuple = time.gmtime(utc_time + timezone_offset) + if _debug: + LocalDateTimeValueObject._debug(" - time_tuple: %r", time_tuple) + + # extra the pieces + date_quad = ( + time_tuple[0] - 1900, + time_tuple[1], + time_tuple[2], + time_tuple[6] + 1, + ) + time_quad = (time_tuple[3], time_tuple[4], time_tuple[5], 0) + + date_time = DateTime(date=date_quad, time=time_quad) + if _debug: + LocalDateTimeValueObject._debug(" - date_time: %r", date_time) + + self.presentValue = date_time + + +# result name, object class [, default units [, metric units, imperial units]] +parameters = [ + ("$.clouds.all", LocalAnalogValueObject, "percent"), + ("$.main.humidity", LocalAnalogValueObject, "percentRelativeHumidity"), + ("$.main.pressure", LocalAnalogValueObject, "hectopascals"), + ( + "$.main.temp", + LocalAnalogValueObject, + "degreesKelvin", + "degreesCelsius", + "degreesFahrenheit", + ), + ( + "$.main.temp_max", + LocalAnalogValueObject, + "degreesKelvin", + "degreesCelsius", + "degreesFahrenheit", + ), + ( + "$.main.temp_min", + LocalAnalogValueObject, + "degreesKelvin", + "degreesCelsius", + "degreesFahrenheit", + ), + ("$.sys.sunrise", LocalDateTimeValueObject), + ("$.sys.sunset", LocalDateTimeValueObject), + ("$.visibility", LocalAnalogValueObject, "meters"), + ("$.wind.deg", LocalAnalogValueObject, "degreesAngular"), + ( + "$.wind.speed", + LocalAnalogValueObject, + "metersPerSecond", + "metersPerSecond", + "milesPerHour", + ), +] + + +@bacpypes_debugging +def create_objects(app): + """Create the objects that hold the result values.""" + if _debug: + create_objects._debug("create_objects %r", app) + global objects + + next_instance = 1 + for parms in parameters: + if _debug: + create_objects._debug(" - name: %r", parms[0]) + + if len(parms) == 2: + units = None + elif len(parms) == 3: + units = parms[2] + elif APPUNITS == "metric": + units = parms[3] + elif APPUNITS == "imperial": + units = parms[4] + else: + units = parms[2] + if _debug: + create_objects._debug(" - units: %r", units) + + # create an object + obj = parms[1]( + objectName=parms[0], objectIdentifier=(parms[1].objectType, next_instance) + ) + if _debug: + create_objects._debug(" - obj: %r", obj) + + # set the units + if units is not None: + obj.units = units + + # add it to the application + app.add_object(obj) + + # keep track of the object by name + objects[parms[0]] = obj + + # bump the next instance number + next_instance += 1 + + +def flatten(x, prefix="$"): + """Turn a JSON object into (key, value) tuples using JSON-Path like names + for the keys.""" + if type(x) is dict: + for a in x: + yield from flatten(x[a], prefix + "." + a) + elif type(x) is list: + for i, y in enumerate(x): + yield from flatten(y, prefix + "[" + str(i) + "]") + else: + yield (prefix, x) + + +@recurring_function(APPINTERVAL) +@bacpypes_debugging +def update_weather_data(): + """Read the current weather data from the API and set the object values.""" + if _debug: + update_weather_data._debug("update_weather_data") + global objects, timezone_offset + + # ask the web service + response = requests.get( + "http://api.openweathermap.org/data/2.5/weather", request_parameters + ) + if response.status_code != 200: + print("Error response: %r" % (response.status_code,)) + return + + # turn the response string into a JSON object + json_response = response.json() + + # flatten the JSON object into key/value pairs and build a dict + dict_response = dict(flatten(json_response)) + + # extract the timezone offset + timezone_offset = dict_response.get("$.timezone", 0) + if _debug: + update_weather_data._debug(" - timezone_offset: %r", timezone_offset) + + # set the object values + for k, v in dict_response.items(): + if _debug: + update_weather_data._debug(" - k, v: %r, %r", k, v) + + if k in objects: + objects[k]._set_value(v) + + +@bacpypes_debugging +def main(): + global vendor_id + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a sample application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # create the objects and add them to the application + create_objects(this_application) + + # run this update when the stack is ready + deferred(update_weather_data) + + if _debug: + _log.debug("running") + + run() + + if _debug: + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index ad1072ee..4da003bb 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -9,7 +9,7 @@ import sys from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolelogging import ConfigArgumentParser, ConsoleLogHandler from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, deferred, enable_sleeping @@ -124,7 +124,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # @@ -133,6 +133,7 @@ def do_rtn(self, args): def main(): global this_application + ConsoleLogHandler('bacpypes.consolelogging') # parse the command line arguments args = ConfigArgumentParser(description=__doc__).parse_args() diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py index b7b54501..56eb40be 100755 --- a/samples/ReadProperty25.py +++ b/samples/ReadProperty25.py @@ -119,7 +119,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) bacpypes_debugging(ReadPropertyConsoleCmd) diff --git a/samples/ReadPropertyForeign.py b/samples/ReadPropertyForeign.py index 5ced6139..513c74f7 100755 --- a/samples/ReadPropertyForeign.py +++ b/samples/ReadPropertyForeign.py @@ -134,7 +134,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/ReadPropertyJSON.py b/samples/ReadPropertyJSON.py new file mode 100644 index 00000000..0d240585 --- /dev/null +++ b/samples/ReadPropertyJSON.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python + +""" +This application presents a 'console' prompt to the user asking for read +commands which create ReadPropertyRequest PDUs, then lines up the +coorresponding ReadPropertyACK and prints the value. + +This version uses the --json option for settings, BACpypes.json rather than +BACpypes.ini. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import JSONArgumentParser, ConsoleLogHandler +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, deferred, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK +from bacpypes.primitivedata import Unsigned, ObjectIdentifier +from bacpypes.constructeddata import Array + +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import get_datatype +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + + +# +# ReadPropertyConsoleCmd +# + + +@bacpypes_debugging +class ReadPropertyConsoleCmd(ConsoleCmd): + def do_read(self, args): + """read [ ]""" + args = args.split() + if _debug: + ReadPropertyConsoleCmd._debug("do_read %r", args) + + try: + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value + + datatype = get_datatype(obj_id[0], prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a request + request = ReadPropertyRequest( + objectIdentifier=obj_id, propertyIdentifier=prop_id + ) + request.pduDestination = Address(addr) + + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) + if _debug: + ReadPropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: + ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + deferred(this_application.request_io, iocb) + + # wait for it to complete + iocb.wait() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + "\n") + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: + ReadPropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype( + apdu.objectIdentifier[0], apdu.propertyIdentifier + ) + if _debug: + ReadPropertyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and ( + apdu.propertyArrayIndex is not None + ): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: + ReadPropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + "\n") + if hasattr(value, "debug_contents"): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something with nothing? + else: + if _debug: + ReadPropertyConsoleCmd._debug( + " - ioError or ioResponse expected" + ) + + except Exception as error: + ReadPropertyConsoleCmd._exception("exception: %r", error) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: + ReadPropertyConsoleCmd._debug("do_rtn %r", args) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.update_router_references( + None, router_address, network_list + ) + + +# +# __main__ +# + + +def main(): + global this_application + + # create a console log handler to show how settings are gathered from + # the JSON file. The settings may include debugging, so this is to + # debug the debugging and usually isn't necessary + ConsoleLogHandler("bacpypes.consolelogging") + + # parse the command line arguments + args = JSONArgumentParser(description=__doc__).parse_args() + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + local_device = args.json["local-device"] + if _debug: + _log.debug(" - local_device: %r", local_device) + + # make a device object + this_device = LocalDeviceObject(**local_device) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = BIPSimpleApplication(this_device, local_device.address) + + # make a console + this_console = ReadPropertyConsoleCmd() + if _debug: + _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py index 88f93ac9..a46f1989 100644 --- a/samples/ReadWriteEventMessageTexts.py +++ b/samples/ReadWriteEventMessageTexts.py @@ -193,7 +193,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index 1754fcb1..7ee7d177 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -243,7 +243,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/RecurringWriteProperty.py b/samples/RecurringWriteProperty.py new file mode 100755 index 00000000..20e04ff1 --- /dev/null +++ b/samples/RecurringWriteProperty.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python + +""" +This application demonstrates writing a series of values at a regular interval. + + $ python RecurringWriteProperty.py 1.2.3.4 analogValue:1 \ + presentValue 1.2 3.4 5.6 +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.task import RecurringTask +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.primitivedata import Real, ObjectIdentifier +from bacpypes.constructeddata import Any +from bacpypes.basetypes import PropertyIdentifier +from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_application = None + + +@bacpypes_debugging +class PrairieDog(RecurringTask): + + """ + An instance of this class pops up out of the ground every once in a + while and write out the next value. + """ + + def __init__(self, interval): + if _debug: + PrairieDog._debug("__init__ %r", interval) + RecurringTask.__init__(self, interval) + + # install it + self.install_task() + + def process_task(self): + if _debug: + PrairieDog._debug("process_task") + global args, this_application + + if _debug: + PrairieDog._debug(" - args.values: %r", args.values) + + # pick up the next value + value = args.values.pop(0) + args.values.append(value) + + # make a primitive value out of it + value = Real(float(value)) + + # build a request + request = WritePropertyRequest( + destination=args.daddr, + objectIdentifier=args.objid, + propertyIdentifier=args.propid, + ) + + # save the value, application tagged + request.propertyValue = Any() + request.propertyValue.cast_in(value) + if _debug: + PrairieDog._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + iocb.add_callback(self.write_complete) + if _debug: + PrairieDog._debug(" - iocb: %r", iocb) + + # give it to the application to process + deferred(this_application.request_io, iocb) + + def write_complete(self, iocb): + if _debug: + PrairieDog._debug("write_complete %r", iocb) + + # do something for success + if iocb.ioResponse: + # should be an ack + if not isinstance(iocb.ioResponse, SimpleAckPDU): + if _debug: + PrairieDog._debug(" - not an ack") + else: + sys.stdout.write("ack\n") + + # do something for error/reject/abort + elif iocb.ioError: + sys.stdout.write(str(iocb.ioError) + "\n") + + +def main(): + global args, this_application + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an argument for seconds per dog + parser.add_argument("daddr", help="destination address") + parser.add_argument("objid", help="object identifier") + parser.add_argument("propid", help="property identifier") + + # list of values to write + parser.add_argument("values", metavar="N", nargs="+", help="values to write") + + # add an argument for seconds between writes + parser.add_argument( + "--delay", type=float, help="delay between writes in seconds", default=5.0 + ) + + # now parse the arguments + args = parser.parse_args() + + # convert the parameters + args.daddr = Address(args.daddr) + args.objid = ObjectIdentifier(args.objid).value + args.propid = PropertyIdentifier(args.propid).value + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # make a dog, task scheduling is in milliseconds + dog = PrairieDog(args.delay * 1000) + if _debug: + _log.debug(" - dog: %r", dog) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/Tutorial/WhoIsIAm.py b/samples/Tutorial/WhoIsIAm.py index 9f35cd48..d58455c5 100644 --- a/samples/Tutorial/WhoIsIAm.py +++ b/samples/Tutorial/WhoIsIAm.py @@ -16,7 +16,6 @@ from bacpypes.pdu import Address, GlobalBroadcast from bacpypes.apdu import WhoIsRequest, IAmRequest -from bacpypes.basetypes import ServicesSupported from bacpypes.errors import DecodingError from bacpypes.app import BIPSimpleApplication @@ -100,7 +99,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -137,7 +135,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/WhoIsIAm.py b/samples/WhoIsIAm.py index 6736da4f..dc07e82e 100755 --- a/samples/WhoIsIAm.py +++ b/samples/WhoIsIAm.py @@ -12,7 +12,7 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, deferred, enable_sleeping +from bacpypes.core import run, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast @@ -158,7 +158,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/WhoIsIAmForeign.py b/samples/WhoIsIAmForeign.py index ad43a210..5c23615d 100755 --- a/samples/WhoIsIAmForeign.py +++ b/samples/WhoIsIAmForeign.py @@ -169,7 +169,7 @@ def do_rtn(self, args): network_list = [int(arg) for arg in args[1:]] # pass along to the service access point - this_application.nsap.add_router_references(None, router_address, network_list) + this_application.nsap.update_router_references(None, router_address, network_list) # diff --git a/samples/WhoIsIAmVLAN.py b/samples/WhoIsIAmVLAN.py new file mode 100755 index 00000000..d9d460f6 --- /dev/null +++ b/samples/WhoIsIAmVLAN.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python + +""" +Who-Is, I-Am VLAN + +This sample application is very similar to WhoIsIAm.py but rather than the +device on an IPv4 network, it is sitting on a VLAN with a router to the +network. The INI file is used for the device object properties with the +exception of the address, which is given to the router. +""" + +import sys +import argparse + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.comm import bind +from bacpypes.core import run, deferred +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address, LocalBroadcast, GlobalBroadcast +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +from bacpypes.app import ApplicationIOController +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import ( + WhoIsIAmServices, + ) +from bacpypes.service.object import ( + ReadWritePropertyServices, + ReadWritePropertyMultipleServices, + ) +from bacpypes.apdu import ( + WhoIsRequest, + IAmRequest, + ) +from bacpypes.errors import DecodingError + +from bacpypes.vlan import Network, Node + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_device = None +this_application = None + +# +# VLANApplication +# + +@bacpypes_debugging +class VLANApplication( + ApplicationIOController, + 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) + ApplicationIOController.__init__(self, vlan_device, vlan_address, aseID=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, address=vlan_address) + + # keep track of requests to line up responses + self._request = None + + if _debug: VLANApplication._debug(" - nsap.local_address: %r", self.nsap.local_address) + + def request(self, apdu): + if _debug: VLANApplication._debug("request %r", apdu) + if _debug: VLANApplication._debug(" - nsap.local_address: %r", self.nsap.local_address) + + # save a copy of the request + self._request = apdu + + # forward it along + super(VLANApplication, self).request(apdu) + + def indication(self, apdu): + if _debug: VLANApplication._debug("indication %r", apdu) + + if (isinstance(self._request, WhoIsRequest)) and (isinstance(apdu, IAmRequest)): + device_type, device_instance = apdu.iAmDeviceIdentifier + if device_type != 'device': + raise DecodingError("invalid object type") + + if (self._request.deviceInstanceRangeLowLimit is not None) and \ + (device_instance < self._request.deviceInstanceRangeLowLimit): + pass + elif (self._request.deviceInstanceRangeHighLimit is not None) and \ + (device_instance > self._request.deviceInstanceRangeHighLimit): + pass + else: + # print out the contents + sys.stdout.write('pduSource = ' + repr(apdu.pduSource) + '\n') + sys.stdout.write('iAmDeviceIdentifier = ' + str(apdu.iAmDeviceIdentifier) + '\n') + sys.stdout.write('maxAPDULengthAccepted = ' + str(apdu.maxAPDULengthAccepted) + '\n') + sys.stdout.write('segmentationSupported = ' + str(apdu.segmentationSupported) + '\n') + sys.stdout.write('vendorID = ' + str(apdu.vendorID) + '\n') + sys.stdout.flush() + + # forward it along + super(VLANApplication, self).indication(apdu) + + def response(self, apdu): + if _debug: VLANApplication._debug("[%s]response %r", self.vlan_node.address, apdu) + super(VLANApplication, self).response(apdu) + + def confirmation(self, apdu): + if _debug: VLANApplication._debug("[%s]confirmation %r", self.vlan_node.address, apdu) + super(VLANApplication, self).confirmation(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) + +# +# WhoIsIAmConsoleCmd +# + +@bacpypes_debugging +class WhoIsIAmConsoleCmd(ConsoleCmd): + + def do_whois(self, args): + """whois [ ] [ ]""" + args = args.split() + if _debug: WhoIsIAmConsoleCmd._debug("do_whois %r", args) + + try: + # build a request + request = WhoIsRequest() + if (len(args) == 1) or (len(args) == 3): + request.pduDestination = Address(args[0]) + del args[0] + else: + request.pduDestination = GlobalBroadcast() + + if len(args) == 2: + request.deviceInstanceRangeLowLimit = int(args[0]) + request.deviceInstanceRangeHighLimit = int(args[1]) + if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: WhoIsIAmConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + except Exception as err: + WhoIsIAmConsoleCmd._exception("exception: %r", err) + + def do_iam(self, args): + """iam""" + args = args.split() + if _debug: WhoIsIAmConsoleCmd._debug("do_iam %r", args) + + try: + # build a request + request = IAmRequest() + request.pduDestination = GlobalBroadcast() + + # set the parameters from the device object + request.iAmDeviceIdentifier = this_device.objectIdentifier + request.maxAPDULengthAccepted = this_device.maxApduLengthAccepted + request.segmentationSupported = this_device.segmentationSupported + request.vendorID = this_device.vendorIdentifier + if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: WhoIsIAmConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + except Exception as err: + WhoIsIAmConsoleCmd._exception("exception: %r", err) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: WhoIsIAmConsoleCmd._debug("do_rtn %r", args) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.update_router_references(None, router_address, network_list) + +# +# __main__ +# + +def main(): + global args, this_device, this_application + + # parse the command line arguments + parser = ConfigArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of IPv4 network', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of VLAN network', + ) + + # add an argument for interval + parser.add_argument('addr2', type=str, + help='address on the VLAN network', + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + local_network = args.net1 + local_address = Address(args.ini.address) + if _debug: _log.debug(" - local_network, local_address: %r, %r", local_network, local_address) + + vlan_network = args.net2 + vlan_address = Address(args.addr2) + if _debug: _log.debug(" - vlan_network, vlan_address: %r, %r", vlan_network, vlan_address) + + # 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) + + # send network topology + deferred(router.nse.i_am_router_to_network) + + # make a vlan device object + this_device = \ + LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=("device", int(args.ini.objectidentifier)), + maxApduLengthAccepted=1024, + segmentationSupported='noSegmentation', + vendorIdentifier=15, + ) + _log.debug(" - this_device: %r", this_device) + + # make the application, add it to the network + this_application = VLANApplication(this_device, vlan_address) + vlan.add_node(this_application.vlan_node) + _log.debug(" - this_application: %r", this_application) + + # make a console + this_console = WhoIsIAmConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/mini_device.py b/samples/mini_device.py new file mode 100755 index 00000000..f25774b1 --- /dev/null +++ b/samples/mini_device.py @@ -0,0 +1,139 @@ +#!/usr/bin/python + +""" +This sample application is a server that supports many core services that +applications need to present data on a BACnet network. It supports Who-Is +and I-Am for device binding, Read and Write Property, Read and Write +Property Multiple, and COV subscriptions. + +Change the process_task() function to do something on a regular INTERVAL +number of seconds. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run +from bacpypes.task import RecurringTask + +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import AnalogValueObject, BinaryValueObject +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.cov import ChangeOfValueServices +from bacpypes.service.object import ReadWritePropertyMultipleServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# settings +INTERVAL = 1.0 + +# globals +test_application = None +test_av = None +test_bv = None + + +@bacpypes_debugging +class SampleApplication( + BIPSimpleApplication, ReadWritePropertyMultipleServices, ChangeOfValueServices +): + pass + + +@bacpypes_debugging +class DoSomething(RecurringTask): + def __init__(self, interval): + if _debug: + DoSomething._debug("__init__ %r", interval) + RecurringTask.__init__(self, interval * 1000) + + # save the interval + self.interval = interval + + # make a list of test values + self.test_values = [ + ("active", 1.0), + ("inactive", 2.0), + ("active", 3.0), + ("inactive", 4.0), + ] + + def process_task(self): + if _debug: + DoSomething._debug("process_task") + global test_av, test_bv + + # pop the next value + next_value = self.test_values.pop(0) + self.test_values.append(next_value) + if _debug: + DoSomething._debug(" - next_value: %r", next_value) + + # change the point + test_av.presentValue = next_value[1] + test_bv.presentValue = next_value[0] + + +def main(): + global test_av, test_bv, test_application + + # make a parser + parser = ConfigArgumentParser(description=__doc__) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a sample application + test_application = SampleApplication(this_device, args.ini.address) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=("analogValue", 1), + objectName="av", + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=1.0, + ) + _log.debug(" - test_av: %r", test_av) + + # add it to the device + test_application.add_object(test_av) + _log.debug(" - object list: %r", this_device.objectList) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=("binaryValue", 1), + objectName="bv", + presentValue="inactive", + statusFlags=[0, 0, 0, 0], + ) + _log.debug(" - test_bv: %r", test_bv) + + # add it to the device + test_application.add_object(test_bv) + + # binary value task + do_something_task = DoSomething(INTERVAL) + do_something_task.install_task() + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/sandbox/vlan_to_vlan.py b/samples/vlan_to_vlan.py similarity index 62% rename from sandbox/vlan_to_vlan.py rename to samples/vlan_to_vlan.py index 4fbe19d6..b312b958 100755 --- a/sandbox/vlan_to_vlan.py +++ b/samples/vlan_to_vlan.py @@ -1,9 +1,20 @@ #!/usr/bin/env python """ -""" +VLAN to VLAN + +This application started out being a way to test various combinations of +traffic before the tests were written. All of the traffic patterns in this +application are in the test suite but this is simpler. + +It doesn't generate any output, turn on debugging to see what each node is +sending (the request() calls) and receiving (the indication() calls). -import sys + $ python vlan_to_vlan.py 1 --debug __main__.VLANApplication + +Note how the source and destination addresses change as packets go through +routers. +""" from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ArgumentParser @@ -11,19 +22,18 @@ from bacpypes.core import run, deferred 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.service.device import WhoIsIAmServices from bacpypes.service.object import ReadWritePropertyServices +from bacpypes.local.device import LocalDeviceObject from bacpypes.apdu import ReadPropertyRequest from bacpypes.vlan import Network, Node -from bacpypes.errors import ExecutionError # some debugging _debug = 0 @@ -33,11 +43,14 @@ # VLANApplication # + @bacpypes_debugging class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): - def __init__(self, objectName, deviceInstance, address, aseID=None): - if _debug: VLANApplication._debug("__init__ %r %r %r aseID=%r", objectName, deviceInstance, address, aseID) + if _debug: + VLANApplication._debug( + "__init__ %r %r %r aseID=%r", objectName, deviceInstance, address, aseID + ) # make an address vlan_address = Address(address) @@ -46,11 +59,11 @@ def __init__(self, objectName, deviceInstance, address, aseID=None): # make a device object vlan_device = LocalDeviceObject( objectName=objectName, - objectIdentifier=('device', deviceInstance), + objectIdentifier=("device", deviceInstance), maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', + segmentationSupported="noSegmentation", vendorIdentifier=15, - ) + ) _log.debug(" - vlan_device: %r", vlan_device) # continue with the initialization @@ -79,36 +92,48 @@ def __init__(self, objectName, deviceInstance, address, aseID=None): # create a vlan node at the assigned address self.vlan_node = Node(vlan_address) - if _debug: VLANApplication._debug(" - vlan_node: %r", self.vlan_node) + if _debug: + VLANApplication._debug(" - vlan_node: %r", self.vlan_node) # bind the stack to the node, no network number self.nsap.bind(self.vlan_node) - if _debug: VLANApplication._debug(" - node bound") + if _debug: + VLANApplication._debug(" - node bound") def request(self, apdu): - if _debug: VLANApplication._debug("[%s]request %r", self.localDevice.objectName, apdu) + if _debug: + VLANApplication._debug("[%s]request %r", self.localDevice.objectName, apdu) Application.request(self, apdu) def indication(self, apdu): - if _debug: VLANApplication._debug("[%s]indication %r", self.localDevice.objectName, apdu) + if _debug: + VLANApplication._debug( + "[%s]indication %r", self.localDevice.objectName, apdu + ) Application.indication(self, apdu) def response(self, apdu): - if _debug: VLANApplication._debug("[%s]response %r", self.localDevice.objectName, apdu) + if _debug: + VLANApplication._debug("[%s]response %r", self.localDevice.objectName, apdu) Application.response(self, apdu) def confirmation(self, apdu): - if _debug: VLANApplication._debug("[%s]confirmation %r", self.localDevice.objectName, apdu) + if _debug: + VLANApplication._debug( + "[%s]confirmation %r", self.localDevice.objectName, apdu + ) + # # VLANRouter # + @bacpypes_debugging class VLANRouter: - def __init__(self): - if _debug: VLANRouter._debug("__init__") + if _debug: + VLANRouter._debug("__init__") # a network service access point will be needed self.nsap = NetworkServiceAccessPoint() @@ -118,7 +143,8 @@ def __init__(self): bind(self.nse, self.nsap) def bind(self, vlan, address, net): - if _debug: VLANRouter._debug("bind %r %r %r", vlan, address, net) + if _debug: + VLANRouter._debug("bind %r %r %r", vlan, address, net) # create a VLAN node for the router with the given address vlan_node = Node(Address(address)) @@ -128,26 +154,27 @@ def bind(self, vlan, address, net): # bind the router stack to the vlan network through this node self.nsap.bind(vlan_node, net) - if _debug: _log.debug(" - bound to vlan") + if _debug: + _log.debug(" - bound to vlan") -# -# __main__ -# def main(): # parse the command line arguments parser = ArgumentParser(description=__doc__) # add an argument for which test to run - parser.add_argument('test_id', type=int, - help='test number', - ) + parser.add_argument("test_id", type=int, help="test number") # now parse the arguments args = parser.parse_args() - if _debug: _log.debug("initialization") - if _debug: _log.debug(" - args: %r", args) + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # VLAN needs to know what a broadcast address looks like + vlan_broadcast_address = LocalBroadcast() # # Router1 @@ -155,26 +182,27 @@ def main(): # create the router router1 = VLANRouter() - if _debug: _log.debug(" - router1: %r", router1) + if _debug: + _log.debug(" - router1: %r", router1) # # VLAN-1 # # create VLAN-1 - vlan1 = Network(name='1') - if _debug: _log.debug(" - vlan1: %r", vlan1) + vlan1 = Network(name="1", broadcast_address=vlan_broadcast_address) + if _debug: + _log.debug(" - vlan1: %r", vlan1) # bind the router to the vlan router1.bind(vlan1, 1, 1) - if _debug: _log.debug(" - router1 bound to VLAN-1") + if _debug: + _log.debug(" - router1 bound to VLAN-1") # make the application, add it to the network vlan1_app = VLANApplication( - objectName="VLAN Node 102", - deviceInstance=102, - address=2, - ) + objectName="VLAN Node 102", deviceInstance=102, address=2 + ) vlan1.add_node(vlan1_app.vlan_node) _log.debug(" - vlan1_app: %r", vlan1_app) @@ -183,19 +211,19 @@ def main(): # # create VLAN-2 - vlan2 = Network(name='2') - if _debug: _log.debug(" - vlan2: %r", vlan2) + vlan2 = Network(name="2", broadcast_address=vlan_broadcast_address) + if _debug: + _log.debug(" - vlan2: %r", vlan2) # bind the router stack to the vlan network through this node router1.bind(vlan2, 1, 2) - if _debug: _log.debug(" - router1 bound to VLAN-2") + if _debug: + _log.debug(" - router1 bound to VLAN-2") # make the application, add it to the network vlan2_app = VLANApplication( - objectName="VLAN Node 202", - deviceInstance=202, - address=2, - ) + objectName="VLAN Node 202", deviceInstance=202, address=2 + ) vlan2.add_node(vlan2_app.vlan_node) _log.debug(" - vlan2_app: %r", vlan2_app) @@ -204,68 +232,67 @@ def main(): # # create VLAN-3 - vlan3 = Network(name='3') - if _debug: _log.debug(" - vlan3: %r", vlan3) + vlan3 = Network(name="3", broadcast_address=vlan_broadcast_address) + if _debug: + _log.debug(" - vlan3: %r", vlan3) # bind the router stack to the vlan network through this node router1.bind(vlan3, 1, 3) - if _debug: _log.debug(" - router1 bound to VLAN-3") + if _debug: + _log.debug(" - router1 bound to VLAN-3") # make a vlan device object - vlan3_device = \ - LocalDeviceObject( - objectName="VLAN Node 302", - objectIdentifier=('device', 302), - maxApduLengthAccepted=1024, - segmentationSupported='noSegmentation', - vendorIdentifier=15, - ) + vlan3_device = LocalDeviceObject( + objectName="VLAN Node 302", + objectIdentifier=("device", 302), + maxApduLengthAccepted=1024, + segmentationSupported="noSegmentation", + vendorIdentifier=15, + ) _log.debug(" - vlan3_device: %r", vlan3_device) # make the application, add it to the network vlan3_app = VLANApplication( - objectName="VLAN Node 302", - deviceInstance=302, - address=2, - ) + objectName="VLAN Node 302", deviceInstance=302, address=2 + ) vlan3.add_node(vlan3_app.vlan_node) _log.debug(" - vlan3_app: %r", vlan3_app) - # # Router2 # # create the router router2 = VLANRouter() - if _debug: _log.debug(" - router2: %r", router2) + if _debug: + _log.debug(" - router2: %r", router2) # bind the router stack to the vlan network through this node router2.bind(vlan3, 255, 3) - if _debug: _log.debug(" - router2 bound to VLAN-3") + if _debug: + _log.debug(" - router2 bound to VLAN-3") # # VLAN-4 # # create VLAN-4 - vlan4 = Network(name='4') - if _debug: _log.debug(" - vlan4: %r", vlan4) + vlan4 = Network(name="4", broadcast_address=vlan_broadcast_address) + if _debug: + _log.debug(" - vlan4: %r", vlan4) # bind the router stack to the vlan network through this node router2.bind(vlan4, 1, 4) - if _debug: _log.debug(" - router2 bound to VLAN-4") + if _debug: + _log.debug(" - router2 bound to VLAN-4") # make the application, add it to the network vlan4_app = VLANApplication( - objectName="VLAN Node 402", - deviceInstance=402, - address=2, - ) + objectName="VLAN Node 402", deviceInstance=402, address=2 + ) vlan4.add_node(vlan4_app.vlan_node) _log.debug(" - vlan4_app: %r", vlan4_app) - # # Test 1 # @@ -274,7 +301,6 @@ def main(): # ask the first device to Who-Is everybody deferred(vlan1_app.who_is) - # # Test 2 # @@ -283,14 +309,13 @@ def main(): # make a read request read_property_request = ReadPropertyRequest( destination=Address("2:2"), - objectIdentifier=('device', 202), - propertyIdentifier='objectName', - ) + objectIdentifier=("device", 202), + propertyIdentifier="objectName", + ) # ask the first device to send it deferred(vlan1_app.request, read_property_request) - # # Test 3 # @@ -299,14 +324,13 @@ def main(): # make a read request read_property_request = ReadPropertyRequest( destination=Address("3:2"), - objectIdentifier=('device', 302), - propertyIdentifier='objectName', - ) + objectIdentifier=("device", 302), + propertyIdentifier="objectName", + ) # ask the first device to send it deferred(vlan1_app.request, read_property_request) - # # Test 4 # @@ -315,14 +339,13 @@ def main(): # make a read request read_property_request = ReadPropertyRequest( destination=Address("4:2"), - objectIdentifier=('device', 402), - propertyIdentifier='objectName', - ) + objectIdentifier=("device", 402), + propertyIdentifier="objectName", + ) # ask the first device to send it deferred(vlan1_app.request, read_property_request) - # # Let the test run # diff --git a/sandbox/commandable_feedback.py b/sandbox/commandable_feedback.py new file mode 100644 index 00000000..d92cd859 --- /dev/null +++ b/sandbox/commandable_feedback.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python + +""" +Commandable Feedback +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run + +from bacpypes.basetypes import StatusFlags + +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import register_object_type +from bacpypes.local.device import LocalDeviceObject +from bacpypes.local.object import BinaryOutputCmdObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# register the classes +register_object_type(LocalDeviceObject, vendor_id=999) + + +@bacpypes_debugging +@register_object_type(vendor_id=999) +class BinaryOutputFeedbackObject(BinaryOutputCmdObject): + def __init__(self, *args, **kwargs): + if _debug: + BinaryOutputFeedbackObject._debug("__init__ %r %r", args, kwargs) + super().__init__(*args, **kwargs) + + # listen for changes to the present value + self._property_monitors["presentValue"].append(self.check_feedback) + + def check_feedback(self, old_value, new_value): + if _debug: + BinaryOutputFeedbackObject._debug( + "check_feedback %r %r", old_value, new_value + ) + + # this is violation of 12.7.8 because the object does not support + # event reporting, but it is here for illustration + if new_value == self.feedbackValue: + self.eventState = "normal" + self.statusFlags["inAlarm"] = False + else: + self.eventState = "offnormal" + self.statusFlags["inAlarm"] = True + + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: + _log.debug("initialization") + if _debug: + _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: + _log.debug(" - this_device: %r", this_device) + + # make a sample application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # make a commandable binary output object, add to the device + boo1 = BinaryOutputFeedbackObject( + objectIdentifier=("binaryOutput", 1), + objectName="boo1", + presentValue="inactive", + eventState="normal", + statusFlags=StatusFlags(), + feedbackValue="inactive", + relinquishDefault="inactive", + minimumOnTime=5, # let it warm up + minimumOffTime=10, # let it cool off + ) + if _debug: + _log.debug(" - boo1: %r", boo1) + this_application.add_object(boo1) + + if _debug: + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index cc1a3bae..6f3d1060 100644 --- a/setup.py +++ b/setup.py @@ -19,6 +19,7 @@ (3, 4): 'py34', (3, 5): 'py34', (3, 6): 'py34', + (3, 7): 'py34', }.get(version_info, None) if not source_folder: raise EnvironmentError("unsupported version of Python") @@ -59,7 +60,7 @@ 'bacpypes.service', ], package_dir={ - 'bacpypes': os.path.join(source_folder, 'bacpypes'), + '': os.path.join(source_folder, ''), }, include_package_data=True, install_requires=requirements, diff --git a/test_script.sh b/test_script.sh index 1304cec5..85ef0f27 100755 --- a/test_script.sh +++ b/test_script.sh @@ -37,14 +37,15 @@ bugfile=${bugfile/.sh/.txt} # debugging file can rotate, set the file size large to keep # it from rotating a lot -export BACPYPES_MAXBYTES=10485760 +export BACPYPES_DEBUG_FILE=$bugfile +export BACPYPES_MAX_BYTES=10485760 # add the modules or classes that need debugging and redirect # the output to the file export BACPYPES_DEBUG=" \ - tests.test_service.helpers.ApplicationNetwork:$bugfile \ - tests.test_service.helpers.SnifferStateMachine:$bugfile \ - tests.state_machine.match_pdu:$bugfile \ + tests.test_service.helpers.ApplicationNetwork \ + tests.test_service.helpers.SnifferStateMachine \ + tests.state_machine.match_pdu \ " # debugging output will open the file 'append' which is diff --git a/tests/__init__.py b/tests/__init__.py index d06f887e..61964885 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -18,6 +18,7 @@ from . import test_pdu from . import test_primitive_data from . import test_constructed_data +from . import test_base_types from . import test_utilities from . import test_vlan diff --git a/tests/test_base_types/__init__.py b/tests/test_base_types/__init__.py new file mode 100644 index 00000000..b20ed1aa --- /dev/null +++ b/tests/test_base_types/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/python + +""" +Test Base Types Module +""" + +from . import test_name_value + diff --git a/tests/test_base_types/test_name_value.py b/tests/test_base_types/test_name_value.py new file mode 100644 index 00000000..0f690313 --- /dev/null +++ b/tests/test_base_types/test_name_value.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Name Value Sequence +------------------------ +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob + +from bacpypes.errors import MissingRequiredParameter +from bacpypes.primitivedata import Tag, TagList, Boolean, Integer, Real, Date, Time +from bacpypes.basetypes import DateTime, NameValue + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +def name_value_encode(obj): + """Encode a NameValue object into a tag list.""" + if _debug: name_value_encode._debug("name_value_encode %r", obj) + + tag_list = TagList() + obj.encode(tag_list) + if _debug: name_value_encode._debug(" - tag_list: %r", tag_list) + + return tag_list + + +@bacpypes_debugging +def name_value_decode(tag_list): + """Decode a tag list into a NameValue.""" + if _debug: name_value_decode._debug("name_value_decode %r", tag_list) + + obj = NameValue() + obj.decode(tag_list) + if _debug: name_value_decode._debug(" - obj: %r", obj) + + return obj + + +@bacpypes_debugging +def name_value_endec(name, value=None): + """Pass the name and value to NameValue and compare the results of encoding + and decoding.""" + if _debug: name_value_endec._debug("name_value_endec %r %r", name, value) + + obj1 = NameValue(name, value) + if _debug: name_value_endec._debug(" - obj1: %r", obj1) + + tag_list = name_value_encode(obj1) + if _debug: name_value_endec._debug(" - tag_list: %r", tag_list) + + obj2 = name_value_decode(tag_list) + if _debug: name_value_endec._debug(" - obj2: %r", obj2) + + assert obj1.name == obj2.name + if obj1.value is None: + assert obj2.value is None + elif isinstance(obj1.value, DateTime): + assert obj1.value.date == obj2.value.date + assert obj1.value.time == obj2.value.time + else: + assert obj1.value.value == obj2.value.value + + +@bacpypes_debugging +class TestNameValue(unittest.TestCase): + + def test_simple_tag(self): + if _debug: TestNameValue._debug("test_simple_tag") + + # just the name + name_value_endec("temp") + + def test_primitive_tag(self): + if _debug: TestNameValue._debug("test_primitive_tag") + + # try the primitive types + name_value_endec("status", Boolean(False)) + name_value_endec("age", Integer(3)) + name_value_endec("experience", Real(73.5)) + + def test_date_time_tag(self): + if _debug: TestNameValue._debug("test_date_time_tag") + + # BACnet Birthday (close) + date_time = DateTime(date=(95, 1, 25, 3), time=(9, 0, 0, 0)) + if _debug: TestNameValue._debug(" - date_time: %r", date_time) + + # try the primitive types + name_value_endec("start", date_time) + diff --git a/tests/test_bvll/helpers.py b/tests/test_bvll/helpers.py index 577c2799..9fe0c2a2 100644 --- a/tests/test_bvll/helpers.py +++ b/tests/test_bvll/helpers.py @@ -28,6 +28,17 @@ _debug = 0 _log = ModuleLogger(globals()) + +class _NetworkServiceElement(NetworkServiceElement): + + """ + This class turns off the deferred startup function call that broadcasts + I-Am-Router-To-Network and Network-Number-Is messages. + """ + + _startup_disabled = True + + # # FauxMultiplexer # @@ -332,6 +343,8 @@ class TestDeviceObject(LocalDeviceObject): @bacpypes_debugging class BIPSimpleApplicationLayerStateMachine(ApplicationServiceElement, ClientStateMachine): + _startup_disabled = True + def __init__(self, address, vlan): if _debug: BIPSimpleApplicationLayerStateMachine._debug("__init__ %r %r", address, vlan) @@ -369,7 +382,7 @@ def __init__(self, address, vlan): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) # bind the top layers @@ -402,6 +415,8 @@ def confirmation(self, apdu): class BIPBBMDApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): + _startup_disabled = True + def __init__(self, address, vlan): if _debug: BIPBBMDApplication._debug("__init__ %r %r", address, vlan) @@ -435,7 +450,7 @@ def __init__(self, address, vlan): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) # bind the top layers diff --git a/tests/test_network/__init__.py b/tests/test_network/__init__.py index b0f72021..7370d3dc 100644 --- a/tests/test_network/__init__.py +++ b/tests/test_network/__init__.py @@ -10,4 +10,5 @@ from . import test_net_4 from . import test_net_5 from . import test_net_6 +from . import test_net_7 diff --git a/tests/test_network/helpers.py b/tests/test_network/helpers.py index 71b1f225..0315720d 100644 --- a/tests/test_network/helpers.py +++ b/tests/test_network/helpers.py @@ -27,6 +27,16 @@ _log = ModuleLogger(globals()) +class _NetworkServiceElement(NetworkServiceElement): + + """ + This class turns off the deferred startup function call that broadcasts + I-Am-Router-To-Network and Network-Number-Is messages. + """ + + _startup_disabled = True + + @bacpypes_debugging class NPDUCodec(Client, Server): @@ -132,7 +142,7 @@ def __init__(self): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) def add_network(self, address, vlan, net): @@ -146,7 +156,7 @@ def add_network(self, address, vlan, net): if _debug: RouterNode._debug(" - node: %r", node) # bind the BIP stack to the local network - self.nsap.bind(node, net) + self.nsap.bind(node, net, address) # # RouterStateMachine @@ -214,7 +224,7 @@ def __init__(self, address, vlan): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) # bind the top layers @@ -232,7 +242,7 @@ def indication(self, apdu): self.receive(apdu) def confirmation(self, apdu): - if _debug: ApplicationLayerStateMachine._debug("confirmation %r %r", apdu) + if _debug: ApplicationLayerStateMachine._debug("confirmation %r", apdu) self.receive(apdu) # @@ -241,6 +251,8 @@ def confirmation(self, apdu): class ApplicationNode(Application, WhoIsIAmServices, ReadWritePropertyServices): + _startup_disabled = True + def __init__(self, address, vlan): if _debug: ApplicationNode._debug("__init__ %r %r", address, vlan) @@ -277,7 +289,7 @@ def __init__(self, address, vlan): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) # bind the top layers diff --git a/tests/test_network/test_net_1.py b/tests/test_network/test_net_1.py index 98a08c5b..540096af 100644 --- a/tests/test_network/test_net_1.py +++ b/tests/test_network/test_net_1.py @@ -105,6 +105,8 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(TNetwork, self).check_for_success() + if _debug: TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + assert all_success diff --git a/tests/test_network/test_net_3.py b/tests/test_network/test_net_3.py index 21b4e715..02971a9c 100644 --- a/tests/test_network/test_net_3.py +++ b/tests/test_network/test_net_3.py @@ -113,6 +113,9 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(TNetwork, self).check_for_success() + if _debug: + TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + assert all_success diff --git a/tests/test_network/test_net_7.py b/tests/test_network/test_net_7.py new file mode 100644 index 00000000..08c828ed --- /dev/null +++ b/tests/test_network/test_net_7.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Network Discovery +---------------------- + +The TD is on network 1 with sniffer1, network 2 has sniffer2, network 3 has +sniffer3. All three networks are connected to one IUT router. +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, btox, xtob + +from bacpypes.comm import Client, Server, bind +from bacpypes.pdu import PDU, Address, LocalBroadcast +from bacpypes.vlan import Network + +from bacpypes.npdu import ( + npdu_types, NPDU, + WhoIsRouterToNetwork, IAmRouterToNetwork, ICouldBeRouterToNetwork, + RejectMessageToNetwork, RouterBusyToNetwork, RouterAvailableToNetwork, + RoutingTableEntry, InitializeRoutingTable, InitializeRoutingTableAck, + EstablishConnectionToNetwork, DisconnectConnectionToNetwork, + WhatIsNetworkNumber, NetworkNumberIs, + ) + +from ..state_machine import match_pdu, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self): + if _debug: TNetwork._debug("__init__") + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # network 1 + self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) + self.vlan1.traffic_log = self.traffic_log + + # network 1 state machine + self.td1 = NetworkLayerStateMachine("1", self.vlan1) + self.append(self.td1) + + # network 2 + self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) + self.vlan2.traffic_log = self.traffic_log + + # network 2 state machine + self.td2 = NetworkLayerStateMachine("2", self.vlan2) + self.append(self.td2) + + # network 3 + self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) + self.vlan3.traffic_log = self.traffic_log + + # network 2 state machine + self.td3 = NetworkLayerStateMachine("3", self.vlan3) + self.append(self.td3) + + # implementation under test + self.iut = RouterNode() + + # add the network connections + self.iut.add_network("4", self.vlan1, 1) + self.iut.add_network("5", self.vlan2, 2) + self.iut.add_network("6", self.vlan3, 3) + + def run(self, time_limit=60.0): + if _debug: TNetwork._debug("run %r", time_limit) + + # run the group + super(TNetwork, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: + TNetwork._debug(" - time machine finished") + for state_machine in self.state_machines: + TNetwork._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + assert all_success + + +@bacpypes_debugging +class TestSimple(unittest.TestCase): + + def test_idle(self): + """Test an idle network, nothing happens is success.""" + if _debug: TestSimple._debug("test_idle") + + # create a network + tnet = TNetwork() + + # all start states are successful + tnet.td1.start_state.success() + tnet.td2.start_state.success() + tnet.td3.start_state.success() + + # run the group + tnet.run() + + +@bacpypes_debugging +class TestNetworkStartup(unittest.TestCase): + + def test_01(self): + """Broadcast I-Am-Router-To-Network messages.""" + if _debug: TestNetworkStartup._debug("test_01") + + # create a network + tnet = TNetwork() + + # test device 1 initiates startup, receives I-Am-Router-To-Network + tnet.td1.start_state.doc("1-1-0") \ + .timeout(1).doc("1-1-1") \ + .call(tnet.iut.nse.startup).doc("1-1-2") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[2, 3], + ).doc("1-1-3") \ + .success() + + # test device 2 receives I-Am-Router-To-Network + tnet.td2.start_state.doc("1-2-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1, 3], + ).doc("1-2-1") \ + .success() + + # test device 3 receives I-Am-Router-To-Network + tnet.td3.start_state.doc("1-3-0") \ + .receive(IAmRouterToNetwork, + iartnNetworkList=[1, 2], + ).doc("1-3-1") \ + .success() + + # run the group + tnet.run() + diff --git a/tests/test_pdu/test_address.py b/tests/test_pdu/test_address.py index 682a24e5..edeb7cd8 100644 --- a/tests/test_pdu/test_address.py +++ b/tests/test_pdu/test_address.py @@ -8,6 +8,7 @@ import unittest +from bacpypes.settings import settings from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.pdu import Address, LocalStation, RemoteStation, \ LocalBroadcast, RemoteBroadcast, GlobalBroadcast @@ -53,7 +54,7 @@ def test_address(self): # null address test_addr = Address() - self.match_address(test_addr, 0, None, 0, '') + self.match_address(test_addr, 0, None, None, None) assert str(test_addr) == "Null" def test_address_int(self): @@ -396,8 +397,6 @@ def test_remote_station(self): # two parameters, correct types with self.assertRaises(TypeError): RemoteStation() - with self.assertRaises(TypeError): - RemoteStation(1, 2, 3) with self.assertRaises(TypeError): RemoteStation('x', 2) @@ -442,6 +441,49 @@ def test_remote_station_bytes(self): self.match_address(test_addr, 4, 1, 6, '01020304bac0') assert str(test_addr) == "1:1.2.3.4" + def test_remote_station_ints_routed(self): + if _debug: TestRemoteStation._debug("test_remote_station_ints_routed") + + if not settings.route_aware: + if _debug: TestRemoteStation._debug(" - not route aware") + return + + # test integer + test_addr = RemoteStation(1, 1, route=Address("1.2.3.4")) + self.match_address(test_addr, 4, 1, 1, '01') + assert str(test_addr) == "1:1@1.2.3.4" + + test_addr = RemoteStation(1, 254, route=Address("1.2.3.4")) + self.match_address(test_addr, 4, 1, 1, 'fe') + assert str(test_addr) == "1:254@1.2.3.4" + + # test station address + with self.assertRaises(ValueError): + RemoteStation(1, -1) + with self.assertRaises(ValueError): + RemoteStation(1, 256) + + def test_remote_station_bytes_routed(self): + if _debug: TestRemoteStation._debug("test_remote_station_bytes_routed") + + if not settings.route_aware: + if _debug: TestRemoteStation._debug(" - not route aware") + return + + # multi-byte strings are hex encoded + test_addr = RemoteStation(1, xtob('0102'), route=Address("1.2.3.4")) + self.match_address(test_addr, 4, 1, 2, '0102') + assert str(test_addr) == "1:0x0102@1.2.3.4" + + test_addr = RemoteStation(1, xtob('010203'), route=Address("1.2.3.4")) + self.match_address(test_addr, 4, 1, 3, '010203') + assert str(test_addr) == "1:0x010203@1.2.3.4" + + # match with an IPv4 address + test_addr = RemoteStation(1, xtob('01020304bac0'), route=Address("1.2.3.4")) + self.match_address(test_addr, 4, 1, 6, '01020304bac0') + assert str(test_addr) == "1:1.2.3.4@1.2.3.4" + @bacpypes_debugging class TestLocalBroadcast(unittest.TestCase, MatchAddressMixin): @@ -449,14 +491,20 @@ class TestLocalBroadcast(unittest.TestCase, MatchAddressMixin): def test_local_broadcast(self): if _debug: TestLocalBroadcast._debug("test_local_broadcast") - # no parameters - with self.assertRaises(TypeError): - LocalBroadcast(1) - test_addr = LocalBroadcast() self.match_address(test_addr, 1, None, None, None) assert str(test_addr) == "*" + def test_local_broadcast_routed(self): + if _debug: TestLocalBroadcast._debug("test_local_broadcast_routed") + + if not settings.route_aware: + if _debug: TestLocalBroadcast._debug(" - not route aware") + return + + test_addr = LocalBroadcast(route=Address("1.2.3.4")) + self.match_address(test_addr, 1, None, None, None) + assert str(test_addr) == "*@1.2.3.4" @bacpypes_debugging class TestRemoteBroadcast(unittest.TestCase, MatchAddressMixin): @@ -481,6 +529,18 @@ def test_remote_broadcast(self): self.match_address(test_addr, 3, 1, None, None) assert str(test_addr) == "1:*" + def test_remote_broadcast_routed(self): + if _debug: TestRemoteBroadcast._debug("test_remote_broadcast_routed") + + if not settings.route_aware: + if _debug: TestRemoteBroadcast._debug(" - not route aware") + return + + # match + test_addr = RemoteBroadcast(1, route=Address("1.2.3.4")) + self.match_address(test_addr, 3, 1, None, None) + assert str(test_addr) == "1:*@1.2.3.4" + @bacpypes_debugging class TestGlobalBroadcast(unittest.TestCase, MatchAddressMixin): @@ -488,14 +548,21 @@ class TestGlobalBroadcast(unittest.TestCase, MatchAddressMixin): def test_global_broadcast(self): if _debug: TestGlobalBroadcast._debug("test_global_broadcast") - # no parameters - with self.assertRaises(TypeError): - GlobalBroadcast(1) - test_addr = GlobalBroadcast() self.match_address(test_addr, 5, None, None, None) assert str(test_addr) == "*:*" + def test_global_broadcast_routed(self): + if _debug: TestGlobalBroadcast._debug("test_global_broadcast_routed") + + if not settings.route_aware: + if _debug: TestGlobalBroadcast._debug(" - not route aware") + return + + test_addr = GlobalBroadcast(route=Address("1.2.3.4")) + self.match_address(test_addr, 5, None, None, None) + assert str(test_addr) == "*:*@1.2.3.4" + @bacpypes_debugging class TestAddressEquality(unittest.TestCase, MatchAddressMixin): @@ -508,8 +575,20 @@ def test_address_equality_str(self): assert Address("*") == LocalBroadcast() assert Address("3:4") == RemoteStation(3, 4) assert Address("5:*") == RemoteBroadcast(5) + assert Address("*:*") == GlobalBroadcast() + def test_address_equality_str_routed(self): + if _debug: TestAddressEquality._debug("test_address_equality_str_routed") + + if not settings.route_aware: + if _debug: TestAddressEquality._debug(" - not route aware") + return + + assert Address("3:4@6.7.8.9") == RemoteStation(3, 4, route=Address("6.7.8.9")) + assert Address("5:*@6.7.8.9") == RemoteBroadcast(5, route=Address("6.7.8.9")) + assert Address("*:*@6.7.8.9") == GlobalBroadcast(route=Address("6.7.8.9")) + def test_address_equality_unicode(self): if _debug: TestAddressEquality._debug("test_address_equality_unicode") diff --git a/tests/test_primitive_data/test_character_string.py b/tests/test_primitive_data/test_character_string.py index 10430561..e2f77689 100644 --- a/tests/test_primitive_data/test_character_string.py +++ b/tests/test_primitive_data/test_character_string.py @@ -6,6 +6,7 @@ ------------------------------------ """ +import sys import unittest from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob @@ -100,6 +101,23 @@ def test_character_string_unicode(self): assert obj.value == u"hello" assert str(obj) == "CharacterString(0,X'68656c6c6f')" + def test_character_string_unicode_with_latin(self): + if _debug: TestCharacterString._debug("test_character_string_unicode_with_latin") + # some controllers encoding character string mixing latin-1 and utf-8 + # try to cover those cases without failing + b = xtob('0030b043') # zero degress celsius + tag = Tag(Tag.applicationTagClass, Tag.characterStringAppTag, len(b), b) + obj = CharacterString() + obj.decode(tag) + assert str(obj) == "CharacterString(0,X'30b043')" + + if sys.version_info[0] == 2: + assert obj.value == "0C" # degree symbol dropped, see unicodedata.normalize() + elif sys.version_info[0] == 3: + assert obj.value == "0°C" + else: + raise RuntimeError("unsupported version") + def test_character_string_tag(self): if _debug: TestCharacterString._debug("test_character_string_tag") diff --git a/tests/test_primitive_data/test_unsigned.py b/tests/test_primitive_data/test_unsigned.py index e33fc515..a26febe3 100644 --- a/tests/test_primitive_data/test_unsigned.py +++ b/tests/test_primitive_data/test_unsigned.py @@ -80,6 +80,7 @@ def test_unsigned(self): assert obj.value == 0 assert Unsigned.is_valid(1) + assert Unsigned.is_valid('1') assert not Unsigned.is_valid(-1) if sys.version[0] == 2: assert Unsigned.is_valid(long(1)) diff --git a/tests/test_service/helpers.py b/tests/test_service/helpers.py index 1f027e18..8bfcd967 100644 --- a/tests/test_service/helpers.py +++ b/tests/test_service/helpers.py @@ -28,6 +28,16 @@ _log = ModuleLogger(globals()) +class _NetworkServiceElement(NetworkServiceElement): + + """ + This class turns off the deferred startup function call that broadcasts + I-Am-Router-To-Network and Network-Number-Is messages. + """ + + _startup_disabled = True + + # # ApplicationNetwork # @@ -267,7 +277,7 @@ def __init__(self, localDevice, vlan): self.nsap = NetworkServiceAccessPoint() # give the NSAP a generic network layer service element - self.nse = NetworkServiceElement() + self.nse = _NetworkServiceElement() bind(self.nse, self.nsap) # bind the top layers diff --git a/tests/test_utilities/test_state_machine.py b/tests/test_utilities/test_state_machine.py index 9ee3827d..ff37f6d6 100644 --- a/tests/test_utilities/test_state_machine.py +++ b/tests/test_utilities/test_state_machine.py @@ -28,7 +28,7 @@ def __init__(self, **kwargs): def __repr__(self): return ''.format(', '.join( - '{}={}'.format(k, v) for k,v in self.__dict__.items(), + ('{}={}'.format(k, v) for k,v in self.__dict__.items()), ))