diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 90604514..70e98038 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 1ececeeb..cdc096ec 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -144,6 +144,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -190,7 +191,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -409,6 +432,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -753,6 +779,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1349,8 +1378,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py25/bacpypes/local/__init__.py b/py25/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py25/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py new file mode 100644 index 00000000..7af52a7d --- /dev/null +++ b/py25/bacpypes/local/device.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentProtocolServicesSupported) + +# +# LocalDeviceObject +# + +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + +bacpypes_debugging(LocalDeviceObject) + diff --git a/py25/bacpypes/local/file.py b/py25/bacpypes/local/file.py new file mode 100644 index 00000000..3792526a --- /dev/null +++ b/py25/bacpypes/local/file.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +bacpypes_debugging(LocalRecordAccessFileObject) + +# +# Local Stream Access File Object Type +# + +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +bacpypes_debugging(LocalStreamAccessFileObject) + diff --git a/py25/bacpypes/local/object.py b/py25/bacpypes/local/object.py new file mode 100644 index 00000000..5b5be5d3 --- /dev/null +++ b/py25/bacpypes/local/object.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(CurrentPropertyList) + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py25/bacpypes/local/schedule.py b/py25/bacpypes/local/schedule.py new file mode 100644 index 00000000..a6f368be --- /dev/null +++ b/py25/bacpypes/local/schedule.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +bacpypes_debugging(date_in_calendar_entry) + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +bacpypes_debugging(LocalScheduleObject) + +# +# LocalScheduleInterpreter +# + +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + +bacpypes_debugging(LocalScheduleInterpreter) + diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 503625e5..12b8d819 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -214,6 +215,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 15fa21db..5367ed05 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -449,6 +452,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -461,6 +466,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1338,6 +1363,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1366,11 +1394,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, ''.join(chr(i) for i in self.value)) @@ -1450,19 +1498,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, ''.join(chr(c) for c in self.value)) diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py index 3c0e9bac..af3d29ae 100644 --- a/py25/bacpypes/service/device.py +++ b/py25/bacpypes/service/device.py @@ -4,137 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - -bacpypes_debugging(LocalDeviceObject) - # # Who-Is I-Am Services # diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index 0ce857a7..a7fe647a 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -20,59 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -bacpypes_debugging(CurrentPropertyList) - -# -# CurrentPropertyListMixIn -# - -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - -bacpypes_debugging(CurrentPropertyListMixIn) - # # ReadProperty and WriteProperty Services # diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 90604514..70e98038 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index c8eb755b..bad87d0d 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -145,6 +145,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -191,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -410,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -749,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1311,7 +1340,7 @@ def dict_contents(self, use_dict=None, as_class=dict): # @bacpypes_debugging -class AnyAtomic: +class AnyAtomic(Atomic): def __init__(self, arg=None): if _debug: AnyAtomic._debug("__init__ %r", arg) @@ -1342,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py27/bacpypes/local/__init__.py b/py27/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py27/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py27/bacpypes/local/device.py b/py27/bacpypes/local/device.py new file mode 100644 index 00000000..f1aef729 --- /dev/null +++ b/py27/bacpypes/local/device.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + diff --git a/py27/bacpypes/local/file.py b/py27/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py27/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py27/bacpypes/local/object.py b/py27/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py27/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py27/bacpypes/local/schedule.py b/py27/bacpypes/local/schedule.py new file mode 100644 index 00000000..91cbee62 --- /dev/null +++ b/py27/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [ord(c) for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 523285cc..dc09e736 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -216,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 2c9f55ce..ccc0d688 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -453,6 +456,8 @@ class Atomic(object): _app_tag = None def __cmp__(self, other): + # sys.stderr.write("__cmp__ %r %r\n" % (self, other)) + # hoop jump it if not isinstance(other, self.__class__): other = self.__class__(other) @@ -465,6 +470,26 @@ def __cmp__(self, other): else: return 0 + def __lt__(self, other): + # sys.stderr.write("__lt__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return (self.value < other.value) + + def __eq__(self, other): + # sys.stderr.write("__eq__ %r %r\n" % (self, other)) + + # hoop jump it + if not isinstance(other, self.__class__): + other = self.__class__(other) + + # now compare the values + return self.value == other.value + @classmethod def coerce(cls, arg): """Given an arg, return the appropriate value given the class.""" @@ -1344,6 +1369,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1372,11 +1400,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1456,19 +1504,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py index 25283a4d..11be52b0 100644 --- a/py27/bacpypes/service/device.py +++ b/py27/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for properties this class implements - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py index ca8d3fe9..5c4c993f 100644 --- a/py27/bacpypes/service/object.py +++ b/py27/bacpypes/service/object.py @@ -7,11 +7,11 @@ from ..primitivedata import Atomic, Null, Unsigned from ..constructeddata import Any, Array, ArrayOf -from ..apdu import Error, \ +from ..apdu import \ SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice from ..errors import ExecutionError -from ..object import Property, Object, PropertyError +from ..object import PropertyError # some debugging _debug = 0 @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 5ea5be2f..a77e8be9 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -69,6 +69,8 @@ from . import app from . import appservice + +from . import local from . import service # diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index c8eb755b..574901ae 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -145,6 +145,7 @@ def decode(self, taglist): for element in self.sequenceElements: tag = taglist.Peek() + if _debug: Sequence._debug(" - element, tag: %r, %r", element, tag) # no more elements if tag is None: @@ -191,7 +192,29 @@ def decode(self, taglist): if tag.tagClass != Tag.closingTagClass or tag.tagNumber != element.context: raise InvalidTag("%s expected closing tag %d" % (element.name, element.context)) - # check for an atomic element + # check for an any atomic element + elif issubclass(element.klass, AnyAtomic): + # convert it to application encoding + if element.context is not None: + raise InvalidTag("%s any atomic with context tag %d" % (element.name, element.context)) + + if tag.tagClass != Tag.applicationTagClass: + if not element.optional: + raise InvalidParameterDatatype("%s expected any atomic application tag" % (element.name,)) + else: + setattr(self, element.name, None) + continue + + # consume the tag + taglist.Pop() + + # a helper cooperates between the atomic value and the tag + helper = element.klass(tag) + + # now save the value + setattr(self, element.name, helper.value) + + # check for specific kind of atomic element, or the context says what kind elif issubclass(element.klass, Atomic): # convert it to application encoding if element.context is not None: @@ -410,6 +433,9 @@ def __len__(self): def __getitem__(self, item): return self.value[item] + def __iter__(self): + return iter(self.value) + def encode(self, taglist): if _debug: _SequenceOf._debug("(%r)encode %r", self.__class__.__name__, taglist) for value in self.value: @@ -749,6 +775,9 @@ def __delitem__(self, item): del self.value[item] self.value[0] -= 1 + def __iter__(self): + return iter(self.value[1:]) + def index(self, value): # only search through values for i in range(1, self.value[0] + 1): @@ -1342,8 +1371,13 @@ def decode(self, tag): # get the data self.value = tag.app_to_object() + @classmethod + def is_valid(cls, arg): + """Return True if arg is valid value for the class.""" + return isinstance(arg, Atomic) and not isinstance(arg, AnyAtomic) + def __str__(self): - return "AnyAtomic(%s)" % (str(self.value), ) + return "%s(%s)" % (self.__class__.__name__, str(self.value)) def __repr__(self): desc = self.__module__ + '.' + self.__class__.__name__ diff --git a/py34/bacpypes/local/__init__.py b/py34/bacpypes/local/__init__.py new file mode 100644 index 00000000..277c3c76 --- /dev/null +++ b/py34/bacpypes/local/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +""" +Local Object Subpackage +""" + +from . import object +from . import device +from . import file +from . import schedule + diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py new file mode 100644 index 00000000..c823b897 --- /dev/null +++ b/py34/bacpypes/local/device.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf +from ..basetypes import ServicesSupported + +from ..errors import ExecutionError +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentLocalDate +# + +class CurrentLocalDate(Property): + + def __init__(self): + Property.__init__(self, 'localDate', Date, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentLocalTime +# + +class CurrentLocalTime(Property): + + def __init__(self): + Property.__init__(self, 'localTime', Time, default=(), optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentProtocolServicesSupported +# + +@bacpypes_debugging +class CurrentProtocolServicesSupported(Property): + + def __init__(self): + if _debug: CurrentProtocolServicesSupported._debug("__init__") + Property.__init__(self, 'protocolServicesSupported', ServicesSupported, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentProtocolServicesSupported._debug("ReadProperty %r %r", obj, arrayIndex) + + # not an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return what the application says + return obj._app.get_services_supported() + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): + + properties = [ + CurrentLocalTime(), + CurrentLocalDate(), + CurrentProtocolServicesSupported(), + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) + del kwargs[key] + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for properties this class implements + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + if 'protocolServicesSupported' in kwargs: + raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") + + # the object identifier is required for the object list + if 'objectIdentifier' not in kwargs: + raise RuntimeError("objectIdentifier is required") + + # coerce the object identifier + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, int): + object_identifier = ('device', object_identifier) + + # the object list is provided + if 'objectList' in kwargs: + raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") + kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + super(LocalDeviceObject, self).__init__(**kwargs) + diff --git a/py34/bacpypes/local/file.py b/py34/bacpypes/local/file.py new file mode 100644 index 00000000..8007bef4 --- /dev/null +++ b/py34/bacpypes/local/file.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + diff --git a/py34/bacpypes/local/object.py b/py34/bacpypes/local/object.py new file mode 100644 index 00000000..3078b9c6 --- /dev/null +++ b/py34/bacpypes/local/object.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..basetypes import PropertyIdentifier +from ..constructeddata import ArrayOf + +from ..errors import ExecutionError +from ..object import Property, Object + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# handy reference +ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) + +# +# CurrentPropertyList +# + +@bacpypes_debugging +class CurrentPropertyList(Property): + + def __init__(self): + if _debug: CurrentPropertyList._debug("__init__") + Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) + + # make a list of the properties that have values + property_list = [k for k, v in obj._values.items() + if v is not None + and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') + ] + if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) + + # sort the list so it's stable + property_list.sort() + + # asking for the whole thing + if arrayIndex is None: + return ArrayOfPropertyIdentifier(property_list) + + # asking for the length + if arrayIndex == 0: + return len(property_list) + + # asking for an index + if arrayIndex > len(property_list): + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + return property_list[arrayIndex - 1] + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentPropertyListMixIn +# + +@bacpypes_debugging +class CurrentPropertyListMixIn(Object): + + properties = [ + CurrentPropertyList(), + ] + diff --git a/py34/bacpypes/local/schedule.py b/py34/bacpypes/local/schedule.py new file mode 100644 index 00000000..d22911a3 --- /dev/null +++ b/py34/bacpypes/local/schedule.py @@ -0,0 +1,558 @@ +#!/usr/bin/env python + +""" +Local Schedule Object +""" + +import sys +import calendar +from time import mktime as _mktime + +from ..debugging import bacpypes_debugging, ModuleLogger + +from ..core import deferred +from ..task import OneShotTask + +from ..primitivedata import Atomic, Null, Unsigned, Date, Time +from ..constructeddata import Array +from ..object import get_datatype, ScheduleObject + +from .object import CurrentPropertyListMixIn + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# match_date +# + +def match_date(date, date_pattern): + """ + Match a specific date, a four-tuple with no special values, with a date + pattern, four-tuple possibly having special values. + """ + # unpack the date and pattern + year, month, day, day_of_week = date + year_p, month_p, day_p, day_of_week_p = date_pattern + + # check the year + if year_p == 255: + # any year + pass + elif year != year_p: + # specific year + return False + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the day + if day_p == 255: + # any day + pass + elif day_p == 32: + # last day of the month + last_day = calendar.monthrange(year + 1900, month)[1] + if day != last_day: + return False + elif day_p == 33: + # odd days of the month + if (day % 2) == 0: + return False + elif day_p == 34: + # even days of the month + if (day % 2) == 1: + return False + elif day != day_p: + # specific day + return False + + # check the day of week + if day_of_week_p == 255: + # any day of the week + pass + elif day_of_week != day_of_week_p: + # specific day of the week + return False + + # all tests pass + return True + +# +# match_date_range +# + +def match_date_range(date, date_range): + """ + Match a specific date, a four-tuple with no special values, with a DateRange + object which as a start date and end date. + """ + return (date[:3] >= date_range.startDate[:3]) \ + and (date[:3] <= date_range.endDate[:3]) + +# +# match_weeknday +# + +def match_weeknday(date, weeknday): + """ + Match a specific date, a four-tuple with no special values, with a + BACnetWeekNDay, an octet string with three (unsigned) octets. + """ + # unpack the date + year, month, day, day_of_week = date + last_day = calendar.monthrange(year + 1900, month)[1] + + # unpack the date pattern octet string + weeknday_unpacked = [c for c in weeknday] + month_p, week_of_month_p, day_of_week_p = weeknday_unpacked + + # check the month + if month_p == 255: + # any month + pass + elif month_p == 13: + # odd months + if (month % 2) == 0: + return False + elif month_p == 14: + # even months + if (month % 2) == 1: + return False + elif month != month_p: + # specific month + return False + + # check the week of the month + if week_of_month_p == 255: + # any week + pass + elif week_of_month_p == 1: + # days numbered 1-7 + if (day > 7): + return False + elif week_of_month_p == 2: + # days numbered 8-14 + if (day < 8) or (day > 14): + return False + elif week_of_month_p == 3: + # days numbered 15-21 + if (day < 15) or (day > 21): + return False + elif week_of_month_p == 4: + # days numbered 22-28 + if (day < 22) or (day > 28): + return False + elif week_of_month_p == 5: + # days numbered 29-31 + if (day < 29) or (day > 31): + return False + elif week_of_month_p == 6: + # last 7 days of this month + if (day < last_day - 6): + return False + elif week_of_month_p == 7: + # any of the 7 days prior to the last 7 days of this month + if (day < last_day - 13) or (day > last_day - 7): + return False + elif week_of_month_p == 8: + # any of the 7 days prior to the last 14 days of this month + if (day < last_day - 20) or (day > last_day - 14): + return False + elif week_of_month_p == 9: + # any of the 7 days prior to the last 21 days of this month + if (day < last_day - 27) or (day > last_day - 21): + return False + + # check the day + if day_of_week_p == 255: + # any day + pass + elif day_of_week != day_of_week_p: + # specific day + return False + + # all tests pass + return True + +# +# date_in_calendar_entry +# + +@bacpypes_debugging +def date_in_calendar_entry(date, calendar_entry): + if _debug: date_in_calendar_entry._debug("date_in_calendar_entry %r %r", date, calendar_entry) + + match = False + if calendar_entry.date: + match = match_date(date, calendar_entry.date) + elif calendar_entry.dateRange: + match = match_date_range(date, calendar_entry.dateRange) + elif calendar_entry.weekNDay: + match = match_weeknday(date, calendar_entry.weekNDay) + else: + raise RuntimeError("") + if _debug: date_in_calendar_entry._debug(" - match: %r", match) + + return match + +# +# datetime_to_time +# + +def datetime_to_time(date, time): + """Take the date and time 4-tuples and return the time in seconds since + the epoch as a floating point number.""" + if (255 in date) or (255 in time): + raise RuntimeError("specific date and time required") + + time_tuple = ( + date[0]+1900, date[1], date[2], + time[0], time[1], time[2], + 0, 0, -1, + ) + return _mktime(time_tuple) + +# +# LocalScheduleObject +# + +@bacpypes_debugging +class LocalScheduleObject(CurrentPropertyListMixIn, ScheduleObject): + + def __init__(self, **kwargs): + if _debug: LocalScheduleObject._debug("__init__ %r", kwargs) + + # make sure present value was provided + if 'presentValue' not in kwargs: + raise RuntimeError("presentValue required") + if not isinstance(kwargs['presentValue'], Atomic): + raise TypeError("presentValue must be an Atomic value") + + # continue initialization + ScheduleObject.__init__(self, **kwargs) + + # attach an interpreter task + self._task = LocalScheduleInterpreter(self) + + # add some monitors to check the reliability if these change + for prop in ('weeklySchedule', 'exceptionSchedule', 'scheduleDefault'): + self._property_monitors[prop].append(self._check_reliability) + + # check it now + self._check_reliability() + + def _check_reliability(self, old_value=None, new_value=None): + """This function is called when the object is created and after + one of its configuration properties has changed. The new and old value + parameters are ignored, this is called after the property has been + changed and this is only concerned with the current value.""" + if _debug: LocalScheduleObject._debug("_check_reliability %r %r", old_value, new_value) + + try: + schedule_default = self.scheduleDefault + + if schedule_default is None: + raise ValueError("scheduleDefault expected") + if not isinstance(schedule_default, Atomic): + raise TypeError("scheduleDefault must be an instance of an atomic type") + + schedule_datatype = schedule_default.__class__ + if _debug: LocalScheduleObject._debug(" - schedule_datatype: %r", schedule_datatype) + + if (self.weeklySchedule is None) and (self.exceptionSchedule is None): + raise ValueError("schedule required") + + # check the weekly schedule values + if self.weeklySchedule: + for daily_schedule in self.weeklySchedule: + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleObject._debug(" - daily time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + elif 255 in time_value.time: + if _debug: LocalScheduleObject._debug(" - wildcard in time") + raise ValueError("must be a specific time") + + # check the exception schedule values + if self.exceptionSchedule: + for special_event in self.exceptionSchedule: + for time_value in special_event.listOfTimeValues: + if _debug: LocalScheduleObject._debug(" - special event time_value: %r", time_value) + if time_value is None: + pass + elif not isinstance(time_value.value, (Null, schedule_datatype)): + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + schedule_datatype, + time_value.__class__, + ) + raise TypeError("wrong type") + + # check list of object property references + obj_prop_refs = self.listOfObjectPropertyReferences + if obj_prop_refs: + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + raise RuntimeError("no external references") + + # get the datatype of the property to be written + obj_type = obj_prop_ref.objectIdentifier[0] + datatype = get_datatype(obj_type, obj_prop_ref.propertyIdentifier) + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if issubclass(datatype, Array) and (obj_prop_ref.propertyArrayIndex is not None): + if obj_prop_ref.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: LocalScheduleObject._debug(" - datatype: %r", datatype) + + if datatype is not schedule_datatype: + if _debug: LocalScheduleObject._debug(" - wrong type: expected %r, got %r", + datatype, + schedule_datatype, + ) + raise TypeError("wrong type") + + # all good + self.reliability = 'noFaultDetected' + if _debug: LocalScheduleObject._debug(" - no fault detected") + + except Exception as err: + if _debug: LocalScheduleObject._debug(" - exception: %r", err) + self.reliability = 'configurationError' + +# +# LocalScheduleInterpreter +# + +@bacpypes_debugging +class LocalScheduleInterpreter(OneShotTask): + + def __init__(self, sched_obj): + if _debug: LocalScheduleInterpreter._debug("__init__ %r", sched_obj) + OneShotTask.__init__(self) + + # reference the schedule object to update + self.sched_obj = sched_obj + + # add a monitor for the present value + sched_obj._property_monitors['presentValue'].append(self.present_value_changed) + + # call to interpret the schedule + deferred(self.process_task) + + def present_value_changed(self, old_value, new_value): + """This function is called when the presentValue of the local schedule + object has changed, both internally by this interpreter, or externally + by some client using WriteProperty.""" + if _debug: LocalScheduleInterpreter._debug("present_value_changed %s %s", old_value, new_value) + + # if this hasn't been added to an application, there's nothing to do + if not self.sched_obj._app: + if _debug: LocalScheduleInterpreter._debug(" - no application") + return + + # process the list of [device] object property [array index] references + obj_prop_refs = self.sched_obj.listOfObjectPropertyReferences + if not obj_prop_refs: + if _debug: LocalScheduleInterpreter._debug(" - no writes defined") + return + + # primitive values just set the value part + new_value = new_value.value + + # loop through the writes + for obj_prop_ref in obj_prop_refs: + if obj_prop_ref.deviceIdentifier: + if _debug: LocalScheduleInterpreter._debug(" - no externals") + continue + + # get the object from the application + obj = self.sched_obj._app.get_object_id(obj_prop_ref.objectIdentifier) + if not obj: + if _debug: LocalScheduleInterpreter._debug(" - no object") + continue + + # try to change the value + try: + obj.WriteProperty( + obj_prop_ref.propertyIdentifier, + new_value, + arrayIndex=obj_prop_ref.propertyArrayIndex, + priority=self.sched_obj.priorityForWriting, + ) + if _debug: LocalScheduleInterpreter._debug(" - success") + except Exception as err: + if _debug: LocalScheduleInterpreter._debug(" - error: %r", err) + + def process_task(self): + if _debug: LocalScheduleInterpreter._debug("process_task(%s)", self.sched_obj.objectName) + + # check for a valid configuration + if self.sched_obj.reliability != 'noFaultDetected': + if _debug: LocalScheduleInterpreter._debug(" - fault detected") + return + + # get the date and time from the device object in case it provides + # some custom functionality + if self.sched_obj._app and self.sched_obj._app.localDevice: + current_date = self.sched_obj._app.localDevice.localDate + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = self.sched_obj._app.localDevice.localTime + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + else: + # get the current date and time, as provided by the task manager + current_date = Date().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_date: %r", current_date) + + current_time = Time().now().value + if _debug: LocalScheduleInterpreter._debug(" - current_time: %r", current_time) + + # evaluate the time + current_value, next_transition = self.eval(current_date, current_time) + if _debug: LocalScheduleInterpreter._debug(" - current_value, next_transition: %r, %r", current_value, next_transition) + + ### set the present value + self.sched_obj.presentValue = current_value + + # compute the time of the next transition + transition_time = datetime_to_time(current_date, next_transition) + + # install this to run again + self.install_task(transition_time) + + def eval(self, edate, etime): + """Evaluate the schedule according to the provided date and time and + return the appropriate present value, or None if not in the effective + period.""" + if _debug: LocalScheduleInterpreter._debug("eval %r %r", edate, etime) + + # reference the schedule object + sched_obj = self.sched_obj + if _debug: LocalScheduleInterpreter._debug(" sched_obj: %r", sched_obj) + + # verify the date falls in the effective period + if not match_date_range(edate, sched_obj.effectivePeriod): + return None + + # the event priority is a list of values that are in effect for + # exception schedules with the special event priority, see 135.1-2013 + # clause 7.3.2.23.10.3.8, Revision 4 Event Priority Test + event_priority = [None] * 16 + + next_day = (24, 0, 0, 0) + next_transition_time = [None] * 16 + + # check the exception schedule values + if sched_obj.exceptionSchedule: + for special_event in sched_obj.exceptionSchedule: + if _debug: LocalScheduleInterpreter._debug(" - special_event: %r", special_event) + + # check the special event period + special_event_period = special_event.period + if special_event_period is None: + raise RuntimeError("special event period required") + + match = False + calendar_entry = special_event_period.calendarEntry + if calendar_entry: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + else: + # get the calendar object from the application + calendar_object = sched_obj._app.get_object_id(special_event_period.calendarReference) + if not calendar_object: + raise RuntimeError("invalid calendar object reference") + if _debug: LocalScheduleInterpreter._debug(" - calendar_object: %r", calendar_object) + + for calendar_entry in calendar_object.dateList: + if _debug: LocalScheduleInterpreter._debug(" - calendar_entry: %r", calendar_entry) + match = date_in_calendar_entry(edate, calendar_entry) + if match: + break + + # didn't match the period, try the next special event + if not match: + if _debug: LocalScheduleInterpreter._debug(" - no matching calendar entry") + continue + + # event priority array index + priority = special_event.eventPriority - 1 + if _debug: LocalScheduleInterpreter._debug(" - priority: %r", priority) + + # look for all of the possible times + for time_value in special_event.listOfTimeValues: + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - relinquish exception @ %r", tval) + event_priority[priority] = None + next_transition_time[priority] = None + else: + if _debug: LocalScheduleInterpreter._debug(" - consider exception @ %r", tval) + event_priority[priority] = time_value.value + next_transition_time[priority] = next_day + else: + next_transition_time[priority] = tval + break + + # assume the next transition will be at the start of the next day + earliest_transition = next_day + + # check if any of the special events came up with something + for priority_value, next_transition in zip(event_priority, next_transition_time): + if next_transition is not None: + earliest_transition = min(earliest_transition, next_transition) + if priority_value is not None: + if _debug: LocalScheduleInterpreter._debug(" - priority_value: %r", priority_value) + return priority_value, earliest_transition + + # start out with the default + daily_value = sched_obj.scheduleDefault + + # check the daily schedule + if sched_obj.weeklySchedule: + daily_schedule = sched_obj.weeklySchedule[edate[3]] + if _debug: LocalScheduleInterpreter._debug(" - daily_schedule: %r", daily_schedule) + + # look for all of the possible times + for time_value in daily_schedule.daySchedule: + if _debug: LocalScheduleInterpreter._debug(" - time_value: %r", time_value) + + tval = time_value.time + if tval <= etime: + if isinstance(time_value.value, Null): + if _debug: LocalScheduleInterpreter._debug(" - back to normal @ %r", tval) + daily_value = sched_obj.scheduleDefault + else: + if _debug: LocalScheduleInterpreter._debug(" - new value @ %r", tval) + daily_value = time_value.value + else: + earliest_transition = min(earliest_transition, tval) + break + + # return what was matched, if anything + return daily_value, earliest_transition + diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index dad8a383..97b58992 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -81,6 +81,7 @@ def _register(xcls): # build a property dictionary by going through the class and all its parents _properties = {} for c in cls.__mro__: + if _debug: register_object_type._debug(" - c: %r", c) for prop in getattr(c, 'properties', []): if prop.identifier not in _properties: _properties[prop.identifier] = prop @@ -215,6 +216,13 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False )) # if it's atomic, make sure it's valid + elif issubclass(self.datatype, AnyAtomic): + if _debug: Property._debug(" - property is any atomic, checking value") + if not isinstance(value, Atomic): + raise InvalidParameterDatatype("%s must be an atomic instance" % ( + self.identifier, + )) + elif issubclass(self.datatype, Atomic): if _debug: Property._debug(" - property is atomic, checking value") if not self.datatype.is_valid(value): diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index 0638de4e..3e89abe1 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -14,6 +14,9 @@ from .errors import DecodingError, InvalidTag, InvalidParameterDatatype from .pdu import PDUData +# import the task manager to get the "current" date and time +from .task import TaskManager as _TaskManager + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -1352,6 +1355,9 @@ def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): elif isinstance(arg, Date): self.value = arg.value + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") @@ -1380,11 +1386,31 @@ def CalcDayOfWeek(self): # put it back together self.value = (year, month, day, day_of_week) - def now(self): - tup = time.localtime() + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) + self.value = (tup[0]-1900, tup[1], tup[2], tup[6] + 1) + return self + def __float__(self): + """Convert to seconds since the epoch.""" + # rip apart the value + year, month, day, day_of_week = self.value + + # check for special values + if (year == 255) or (month in _special_mon_inv) or (day in _special_day_inv): + raise ValueError("no wildcard values") + + # convert to time.time() value + return time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.dateAppTag, bytearray(self.value)) @@ -1464,19 +1490,40 @@ def __init__(self, arg=None, hour=255, minute=255, second=255, hundredth=255): tup_list[3] = tup_list[3] * 10 self.value = tuple(tup_list) + elif isinstance(arg, Time): self.value = arg.value + + elif isinstance(arg, float): + self.now(arg) + else: raise TypeError("invalid constructor datatype") - def now(self): - now = time.time() - tup = time.localtime(now) + def now(self, when=None): + """Set the current value to the correct tuple based on the seconds + since the epoch. If 'when' is not provided, get the current time + from the task manager. + """ + if when is None: + when = _TaskManager().get_time() + tup = time.localtime(when) - self.value = (tup[3], tup[4], tup[5], int((now - int(now)) * 100)) + self.value = (tup[3], tup[4], tup[5], int((when - int(when)) * 100)) return self + def __float__(self): + """Return the current value as an offset from midnight.""" + if 255 in self.value: + raise ValueError("no wildcard values") + + # rip it apart + hour, minute, second, hundredth = self.value + + # put it together + return (hour * 3600.0) + (minute * 60.0) + second + (hundredth / 100.0) + def encode(self, tag): # encode the tag tag.set_app_data(Tag.timeAppTag, bytearray(self.value)) diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py index 7a97051f..11be52b0 100644 --- a/py34/bacpypes/service/device.py +++ b/py34/bacpypes/service/device.py @@ -4,136 +4,16 @@ from ..capability import Capability from ..pdu import GlobalBroadcast -from ..primitivedata import Date, Time, ObjectIdentifier -from ..constructeddata import ArrayOf -from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU, Error +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest, SimpleAckPDU from ..errors import ExecutionError, InconsistentParameters, \ MissingRequiredParameter, ParameterOutOfRange -from ..object import register_object_type, registered_object_types, \ - Property, DeviceObject from ..task import FunctionTask -from .object import CurrentPropertyListMixIn - # some debugging _debug = 0 _log = ModuleLogger(globals()) -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=(), optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, int): - object_identifier = ('device', object_identifier) - - # the object list is provided - if 'objectList' in kwargs: - raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) - # # Who-Is I-Am Services # diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py index ca8d3fe9..3289075d 100755 --- a/py34/bacpypes/service/object.py +++ b/py34/bacpypes/service/object.py @@ -20,57 +20,6 @@ # handy reference ArrayOfPropertyIdentifier = ArrayOf(PropertyIdentifier) -# -# CurrentPropertyList -# - -@bacpypes_debugging -class CurrentPropertyList(Property): - - def __init__(self): - if _debug: CurrentPropertyList._debug("__init__") - Property.__init__(self, 'propertyList', ArrayOfPropertyIdentifier, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentPropertyList._debug("ReadProperty %r %r", obj, arrayIndex) - - # make a list of the properties that have values - property_list = [k for k, v in obj._values.items() - if v is not None - and k not in ('objectName', 'objectType', 'objectIdentifier', 'propertyList') - ] - if _debug: CurrentPropertyList._debug(" - property_list: %r", property_list) - - # sort the list so it's stable - property_list.sort() - - # asking for the whole thing - if arrayIndex is None: - return ArrayOfPropertyIdentifier(property_list) - - # asking for the length - if arrayIndex == 0: - return len(property_list) - - # asking for an index - if arrayIndex > len(property_list): - raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return property_list[arrayIndex - 1] - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentPropertyListMixIn -# - -@bacpypes_debugging -class CurrentPropertyListMixIn(Object): - - properties = [ - CurrentPropertyList(), - ] - # # ReadProperty and WriteProperty Services # diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index a03026ea..01130394 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -15,7 +15,7 @@ from bacpypes.object import AccumulatorObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.object import ReadWritePropertyMultipleServices # some debugging @@ -84,13 +84,6 @@ def main(): # add the additional service this_application.add_capability(ReadWritePropertyMultipleServices) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a random input object accumulator = AccumulatorObject( objectIdentifier=('accumulator', 1), diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index 31095626..492a950e 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -24,7 +24,8 @@ from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint -from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import WhoIsIAmServices from bacpypes.service.object import ReadWritePropertyServices from bacpypes.primitivedata import Real diff --git a/samples/COVClient.py b/samples/COVClient.py index 29281fa4..e79ba964 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -20,7 +20,7 @@ SimpleAckPDU, RejectPDU, AbortPDU from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -228,13 +228,6 @@ def main(): # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = SubscribeCOVConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/COVServer.py b/samples/COVServer.py index c1c2de8e..ad2558a4 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -18,7 +18,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import AnalogValueObject, BinaryValueObject -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject from bacpypes.service.cov import ChangeOfValueServices # some debugging diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 16b13401..41b8998c 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -26,8 +26,8 @@ TimeValueObject, TimePatternValueObject, ChannelObject from bacpypes.app import BIPSimpleApplication -from bacpypes.service.object import CurrentPropertyListMixIn -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.object import CurrentPropertyListMixIn +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index a5247559..e4ce6d63 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -18,7 +18,7 @@ from bacpypes.apdu import DeviceCommunicationControlRequest from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -134,13 +134,6 @@ def main(): # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DCCConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index 7c6b1c82..e0f7276b 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -22,7 +22,7 @@ from bacpypes.errors import DecodingError from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -211,13 +211,6 @@ def main(): # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index fa315b9b..1e7b977e 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -22,7 +22,7 @@ from bacpypes.errors import DecodingError from bacpypes.app import BIPForeignApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -215,13 +215,6 @@ def main(): int(args.ini.foreignttl), ) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make a console this_console = DiscoveryConsoleCmd() if _debug: _log.debug(" - this_console: %r", this_console) diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 9d99cb4b..61d1c21e 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -22,7 +22,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -190,13 +190,6 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # local host, special port HOST, PORT = "", int(args.port) server = ThreadedTCPServer((HOST, PORT), ThreadedHTTPRequestHandler) diff --git a/samples/HandsOnLab/Sample1_SimpleApplication.py b/samples/HandsOnLab/Sample1_SimpleApplication.py index b96478b7..dd02bd3c 100644 --- a/samples/HandsOnLab/Sample1_SimpleApplication.py +++ b/samples/HandsOnLab/Sample1_SimpleApplication.py @@ -13,7 +13,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 1 @@ -72,13 +72,6 @@ def main(): this_application = SampleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py index 27d00499..2a185f96 100644 --- a/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py +++ b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py @@ -15,7 +15,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -90,13 +90,6 @@ def main(): # make a sample application this_application = WhoIsIAmApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") run() diff --git a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py index 9c06c0b2..c44e8dd0 100644 --- a/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py +++ b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py @@ -16,7 +16,7 @@ from bacpypes.core import run from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -92,13 +92,6 @@ def main(): # make a sample application this_application = WhoHasIHaveApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - _log.debug("running") # run until stopped, ^C works diff --git a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py index 8aeb5a44..c5de7083 100644 --- a/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py +++ b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py @@ -21,7 +21,7 @@ from bacpypes.errors import ExecutionError from bacpypes.app import BIPSimpleApplication -from bacpypes.service.device import LocalDeviceObject +from bacpypes.local.device import LocalDeviceObject # some debugging _debug = 0 @@ -99,13 +99,6 @@ def main(): # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) - # get the services supported - services_supported = this_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - this_device.protocolServicesSupported = services_supported.value - # make some random input objects for i in range(1, RANDOM_OBJECT_COUNT+1): ravo = RandomAnalogValueObject( diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py new file mode 100644 index 00000000..20e6c48b --- /dev/null +++ b/samples/LocalScheduleObject.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python + +""" +This application creates a series of Local Schedule Objects and then prompts +to test dates and times. +""" + +from time import localtime as _localtime + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run + +from bacpypes.primitivedata import Null, Integer, Real, Date, Time, CharacterString +from bacpypes.constructeddata import ArrayOf, SequenceOf +from bacpypes.basetypes import CalendarEntry, DailySchedule, DateRange, \ + DeviceObjectPropertyReference, SpecialEvent, SpecialEventPeriod, TimeValue + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject +from bacpypes.local.schedule import LocalScheduleObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +schedule_objects = [] + +# +# TestConsoleCmd +# + +@bacpypes_debugging +class TestConsoleCmd(ConsoleCmd): + + def do_test(self, args): + """test