From 8b1ae6617dc99dbbcc79a093fc329030d89db464 Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 19 Mar 2019 21:56:07 -0400 Subject: [PATCH 1/4] try putting low and high limits on Unsigned (#258) --- py25/bacpypes/basetypes.py | 3 +- py25/bacpypes/primitivedata.py | 44 ++++++++++++++-------- py27/bacpypes/basetypes.py | 3 +- py27/bacpypes/primitivedata.py | 38 +++++++++++++------ py34/bacpypes/basetypes.py | 3 +- py34/bacpypes/primitivedata.py | 28 +++++++++++--- tests/test_primitive_data/test_unsigned.py | 18 ++++++++- 7 files changed, 101 insertions(+), 36 deletions(-) diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index dcbd5ed..c5419a1 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -1730,7 +1730,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index bf3bf12..6718c61 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -594,34 +594,34 @@ class Boolean(Atomic): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): - self.value = 0L + def __init__(self, arg=None): + self.value = 0 if arg is None: pass elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") - self.value = long(arg) - elif isinstance(arg, long): - if (arg < 0): - raise ValueError("unsigned integer required") + 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") self.value = arg.value else: raise TypeError("invalid constructor datatype") def encode(self, tag): # rip apart the number - data = struct.pack('>L', self.value) + data = bytearray(struct.pack('>L', self.value)) # reduce the value to the smallest number of octets - while (len(data) > 1) and (data[0] == '\x00'): - data = data[1:] + while (len(data) > 1) and (data[0] == 0): + del data[0] # encode the tag tag.set_app_data(Tag.unsignedAppTag, data) @@ -633,9 +633,9 @@ class Unsigned(Atomic): raise InvalidTag("invalid tag length") # get the data - rslt = 0L + rslt = 0 for c in tag.tagData: - rslt = (rslt << 8) + ord(c) + rslt = (rslt << 8) + c # save the result self.value = rslt @@ -643,10 +643,24 @@ class Unsigned(Atomic): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, (int, long)) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, int) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index dcbd5ed..c5419a1 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -1730,7 +1730,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 8d00be6..53034e0 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -598,23 +598,23 @@ class Boolean(Atomic): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): - self.value = 0L + def __init__(self, arg=None): + self.value = 0 if arg is None: pass elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") - self.value = long(arg) - elif isinstance(arg, long): - if (arg < 0): - raise ValueError("unsigned integer required") + 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") self.value = arg.value else: raise TypeError("invalid constructor datatype") @@ -637,9 +637,9 @@ class Unsigned(Atomic): raise InvalidTag("invalid tag length") # get the data - rslt = 0L + rslt = 0 for c in tag.tagData: - rslt = (rslt << 8) + ord(c) + rslt = (rslt << 8) + c # save the result self.value = rslt @@ -647,10 +647,24 @@ class Unsigned(Atomic): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, (int, long)) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, int) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index dcbd5ed..c5419a1 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -1730,7 +1730,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index bb46f08..d3b341c 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -598,8 +598,10 @@ class Boolean(Atomic): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): + def __init__(self, arg=None): self.value = 0 if arg is None: @@ -607,10 +609,12 @@ class Unsigned(Atomic): elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") + 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") self.value = arg.value else: raise TypeError("invalid constructor datatype") @@ -643,10 +647,24 @@ class Unsigned(Atomic): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, int) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, int) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/tests/test_primitive_data/test_unsigned.py b/tests/test_primitive_data/test_unsigned.py index 65d6935..e33fc51 100644 --- a/tests/test_primitive_data/test_unsigned.py +++ b/tests/test_primitive_data/test_unsigned.py @@ -12,7 +12,7 @@ import unittest from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.errors import InvalidTag -from bacpypes.primitivedata import Unsigned, Tag +from bacpypes.primitivedata import Unsigned, Unsigned8, Unsigned16, Tag # some debugging _debug = 0 @@ -104,6 +104,22 @@ class TestUnsigned(unittest.TestCase): with self.assertRaises(ValueError): Unsigned(-1) + def test_unsigned8(self): + if _debug: TestUnsigned._debug("test_unsigned8") + + with self.assertRaises(ValueError): + Unsigned8(-1) + with self.assertRaises(ValueError): + Unsigned8(256) + + def test_unsigned16(self): + if _debug: TestUnsigned._debug("test_unsigned16") + + with self.assertRaises(ValueError): + Unsigned16(-1) + with self.assertRaises(ValueError): + Unsigned16(65536) + def test_unsigned_tag(self): if _debug: TestUnsigned._debug("test_unsigned_tag") From 7bd978ffaa61f1f89e512c6d8f4fb112bf264a7f Mon Sep 17 00:00:00 2001 From: Joel Bender Date: Tue, 19 Mar 2019 22:43:19 -0400 Subject: [PATCH 2/4] a little too aggressive copy/paste (#258) --- py25/bacpypes/primitivedata.py | 16 ++++++++++------ py27/bacpypes/primitivedata.py | 12 ++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 6718c61..668c99a 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -598,13 +598,17 @@ class Unsigned(Atomic): _high_limit = None def __init__(self, arg=None): - self.value = 0 + self.value = 0L if arg is None: pass elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): + if not self.is_valid(arg): + raise ValueError("value out of range") + self.value = long(arg) + elif isinstance(arg, long): if not self.is_valid(arg): raise ValueError("value out of range") self.value = arg @@ -617,10 +621,10 @@ class Unsigned(Atomic): def encode(self, tag): # rip apart the number - data = bytearray(struct.pack('>L', self.value)) + data = struct.pack('>L', self.value) # reduce the value to the smallest number of octets - while (len(data) > 1) and (data[0] == 0): + while (len(data) > 1) and (data[0] == '\x00'): del data[0] # encode the tag @@ -633,9 +637,9 @@ class Unsigned(Atomic): raise InvalidTag("invalid tag length") # get the data - rslt = 0 + rslt = 0L for c in tag.tagData: - rslt = (rslt << 8) + c + rslt = (rslt << 8) + ord(c) # save the result self.value = rslt @@ -643,7 +647,7 @@ class Unsigned(Atomic): @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 not isinstance(arg, (int, long)) or isinstance(arg, bool): return False if (arg < cls._low_limit): return False diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 53034e0..43c09e9 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -602,13 +602,17 @@ class Unsigned(Atomic): _high_limit = None def __init__(self, arg=None): - self.value = 0 + self.value = 0L if arg is None: pass elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): + if not self.is_valid(arg): + raise ValueError("value out of range") + self.value = long(arg) + elif isinstance(arg, long): if not self.is_valid(arg): raise ValueError("value out of range") self.value = arg @@ -637,9 +641,9 @@ class Unsigned(Atomic): raise InvalidTag("invalid tag length") # get the data - rslt = 0 + rslt = 0L for c in tag.tagData: - rslt = (rslt << 8) + c + rslt = (rslt << 8) + ord(c) # save the result self.value = rslt @@ -647,7 +651,7 @@ class Unsigned(Atomic): @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 not isinstance(arg, (int, long)) or isinstance(arg, bool): return False if (arg < cls._low_limit): return False From 392a1dd38d36453878f2f9562efc59c1c77485ef Mon Sep 17 00:00:00 2001 From: tjohnsonhvac <36667609+tjohnsonhvac@users.noreply.github.com> Date: Mon, 25 Mar 2019 08:11:55 -0400 Subject: [PATCH 3/4] Update basetypes.py Add in supporting classes for network port object --- py34/bacpypes/basetypes.py | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index c5419a1..e6909a2 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -1565,11 +1565,107 @@ class WriteStatus(Enumerated): , 'successful':2 , 'failed':3 } +class NetworkType(Enumerated): + enumerations = \ + { 'ethernet':0 + , 'arcnet':1 + , 'mstp':2 + , 'ptp':3 + , 'lontalk':4 + , 'ipv4':5 + , 'zigbee':6 + , 'virtual': 7 + # , 'non-bacnet': 8 Removed in Version 1, Revision 18 + , 'ipv6':9 + , 'serial':10 + } +class ProtocolLevel(Enumerated): + enumerations = \ + { 'physical':0 + , 'protocol':1 + , 'bacnetApplication':2 + , 'nonBacnetApplication':3 + } + +class NetworkNumberQuality(Enumerated): + enumerations = \ + { 'unknown':0 + , 'learned':1 + , 'learnedConfigured':2 + , 'configured':3 + } + +class NetworkPortCommand(Enumerated): + enumerations = \ + { 'idle':0 + , 'discardChanges':1 + , 'renewFdDRegistration':2 + , 'restartSlaveDiscovery':3 + , 'renewDHCP':4 + , 'restartAutonegotiation':5 + , 'disconnect':6 + , 'restartPort':7 + } + +class IPMode(Enumerated): + enumerations = \ + { 'normal':0 + , 'foreign':1 + , 'bbmd':2 + } + +class RouterEntryStatus(Enumerated): + # This was defined directly in the RouterEntry Sequence in the standard, but I moved it up here because + # I didn't see anywhere else you defined something that way. + enumerations = \ + { 'available':0 + , 'busy':1 + , 'disconnected':2 + } + # # Forward Sequences # +class HostNPort(Sequence): + sequenceElements = \ + [ Element('host', HostAddress) + , Element('port', Unsigned16) + ] + +class BDTEntry(Sequence): + sequenceElements = \ + [ Element('bbmdAddress', HostNPort) + , Element('broadcastMask', OctetString) # shall be present if BACnet/IP, and absent for BACnet/IPv6 + ] + +class FDTEntry(Sequence): + sequenceElements = \ + [ Element('bacnetIPAddress', OctetString) # the 6-octet B/IP or 18-octet B/IPv6 address of the registrant + , Element('timeToLive', Unsigned16) # time to live in seconds at the time of registration + , Element('remainingTimeToLive', Unsigned16) # remaining time to live in seconds, incl. grace period + ] + +class VMACEntry(Sequence): + sequenceElements = \ + [ Element('virtualMACAddress', OctetString) # maximum size 6 octets + , Element('nativeMACAddress', OctetString) + ] + +class RouterEntry(Sequence): + sequenceElements = \ + [ Element('networkNumber', Unsigned16) + , Element('macAddress', OctetString) + , Element('status', RouterEntryStatus) # Defined Above + ] + +class NameValue(Sequence): + sequenceElements = \ + [ Element('name', CharacterString) + , Element('value', Atomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + ] + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) @@ -1624,6 +1720,13 @@ class ObjectPropertyReference(Sequence): , Element('propertyIdentifier', PropertyIdentifier, 1) , Element('propertyArrayIndex', Unsigned, 2, True) ] + +class HostAddress(Choice): + choiceElements = \ + [ Element('none', Null) + , Element('ipAddress', OctetString) # 4 octets for B/IP or 16 octets for B/IPv6 + , Element('name', CharacterString) # Internet host name (see RFC 1123) + ] class ProcessIdSelection(Choice): choiceElements = \ From bb47c7cdd31462218667b04f3b1237b70eacefee Mon Sep 17 00:00:00 2001 From: tjohnsonhvac <36667609+tjohnsonhvac@users.noreply.github.com> Date: Mon, 25 Mar 2019 14:53:32 -0400 Subject: [PATCH 4/4] Update basetypes.py Added to property ids and objects supported. --- py34/bacpypes/basetypes.py | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index e6909a2..c00ca63 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -94,6 +94,7 @@ class ObjectTypesSupported(BitString): , 'accessUser':35 , 'accessZone':36 , 'credentialDataInput':37 + , 'networkPort':56 , 'networkSecurity':38 , 'bitstringValue':39 , 'characterstringValue':40 @@ -1101,9 +1102,50 @@ class ProgramState(Enumerated): } class PropertyIdentifier(Enumerated): + # TODO: Sort Alphabetically vendor_range = (512, 4194303) enumerations = \ { 'absenteeLimit':244 + , 'tags':486 + , 'profileLocation':91 + , 'eventDetectionEnabled':353 + , 'apduLength':388 + , 'linkSpeed':420 + , 'linkSpeeds':421 + , 'linkSpeedAutonegotiate':422 + , 'networkInterfaceName':424 + , 'bacnetIPMode':408 + , 'ipAddress':400 + , 'bacnetIPUDPPort':412 + , 'ipSubnetMask':411 + , 'ipDefaultGateway':401 + , 'bacnetIPMulticastAddress':409 + , 'ipDNSServer':406 + , 'ipDHCPEnable':402 + , 'ipDHCPLeaseTime':403 + , 'ipDHCPLeaseTimeRemaining':404 + , 'ipDHCPServer':405 + , 'bacnetIPNATTraversal':410 + , 'bacnetIPGlobalAddress':407 + , 'bbmdBroadcastDistributionTable':414 + , 'bbmdAcceptFDRegistrations':413 + , 'bbmdForeignDeviceTable':415 + , 'fdBBMDAddress':418 + , 'fdSubscriptionLifetime':419 + , 'bacnetIPv6Mode':435 + , 'ipv6Address':436 + , 'ipv6PrefixLength':437 + , 'bacnetIPv6UDPPort':438 + , 'ipv6DefaultGateway':439 + , 'bacnetIPv6MulticastAddress':440 + , 'ipv6DNSServer':441 + , 'ipv6AutoAddressingEnabled':442 + , 'ipv6DHCPLeaseTime':443 + , 'ipv6DHCPLeaseTimeRemaining':444 + , 'ipv6DHCPServer':445 + , 'ipv6ZoneIndex':446 + , 'virtualMACAddressTable':429 + , 'routingTable':428 , 'acceptedModes':175 , 'accessAlarmEvents':245 , 'accessDoors':246 @@ -1158,8 +1200,10 @@ class PropertyIdentifier(Enumerated): , 'bufferSize':126 , 'changeOfStateCount':15 , 'changeOfStateTime':16 + , 'changesPending':416 , 'channelNumber':366 , 'clientCovIncrement':127 + , 'command':417 , 'configurationFiles':154 , 'controlGroups':367 , 'controlledVariableReference':19 @@ -1286,6 +1330,7 @@ class PropertyIdentifier(Enumerated): , 'loggingRecord':184 , 'loggingType':197 , 'lowLimit':59 + , 'macAddress':423 , 'maintenanceRequired':158 , 'manipulatedVariableReference':60 , 'manualSlaveAddressBinding':170 @@ -1317,6 +1362,9 @@ class PropertyIdentifier(Enumerated): , 'musterPoint':287 , 'negativeAccessRules':288 , 'networkAccessSecurityPolicies':332 + , 'networkNumber':425 + , 'networkNumberQuality':427 + , 'networkType': 427 , 'nodeSubtype':207 , 'nodeType':208 , 'notificationClass':17 @@ -1365,6 +1413,7 @@ class PropertyIdentifier(Enumerated): , 'propertyList':371 , 'proportionalConstant':93 , 'proportionalConstantUnits':94 + , 'protocolLevel':482 , 'protocolObjectTypesSupported':96 , 'protocolRevision':139 , 'protocolServicesSupported':97 @@ -1376,6 +1425,7 @@ class PropertyIdentifier(Enumerated): , 'recipientList':102 , 'recordsSinceNotification':140 , 'recordCount':141 + , 'referencePort':483 , 'reliability':103 , 'reliabilityEvaluationInhibit':357 , 'relinquishDefault':104