1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-09-28 22:15:23 +08:00
bacpypes/py27/bacpypes/service/cov.py
2018-08-07 23:55:17 -04:00

749 lines
25 KiB
Python

#!/usr/bin/env python
"""
Change Of Value Service
"""
from ..debugging import bacpypes_debugging, DebugContents, ModuleLogger
from ..capability import Capability
from ..core import deferred
from ..task import OneShotTask, RecurringFunctionTask, TaskManager
from ..iocb import IOCB
from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \
Recipient, RecipientProcess, ObjectPropertyReference
from ..constructeddata import ListOf, Any
from ..apdu import ConfirmedCOVNotificationRequest, \
UnconfirmedCOVNotificationRequest, \
SimpleAckPDU, Error, RejectPDU, AbortPDU
from ..errors import ExecutionError
from ..object import Property
from .detect import DetectionAlgorithm, monitor_filter
# some debugging
_debug = 0
_log = ModuleLogger(globals())
#
# SubscriptionList
#
@bacpypes_debugging
class SubscriptionList:
def __init__(self):
if _debug: SubscriptionList._debug("__init__")
self.cov_subscriptions = []
def append(self, cov):
if _debug: SubscriptionList._debug("append %r", cov)
self.cov_subscriptions.append(cov)
def remove(self, cov):
if _debug: SubscriptionList._debug("remove %r", cov)
self.cov_subscriptions.remove(cov)
def find(self, client_addr, proc_id, obj_id):
if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id)
for cov in self.cov_subscriptions:
all_equal = (cov.client_addr == client_addr) and \
(cov.proc_id == proc_id) and \
(cov.obj_id == obj_id)
if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal)
if all_equal:
return cov
return None
def __len__(self):
if _debug: SubscriptionList._debug("__len__")
return len(self.cov_subscriptions)
def __iter__(self):
if _debug: SubscriptionList._debug("__iter__")
for cov in self.cov_subscriptions:
yield cov
#
# Subscription
#
@bacpypes_debugging
class Subscription(OneShotTask, DebugContents):
_debug_contents = (
'obj_ref',
'client_addr',
'proc_id',
'obj_id',
'confirmed',
'lifetime',
)
def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime):
if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime)
OneShotTask.__init__(self)
# save the reference to the related object
self.obj_ref = obj_ref
# save the parameters
self.client_addr = client_addr
self.proc_id = proc_id
self.obj_id = obj_id
self.confirmed = confirmed
self.lifetime = lifetime
# if lifetime is non-zero, schedule the subscription to expire
if lifetime != 0:
self.install_task(delta=self.lifetime)
def cancel_subscription(self):
if _debug: Subscription._debug("cancel_subscription")
# suspend the task
self.suspend_task()
# tell the application to cancel us
self.obj_ref._app.cancel_subscription(self)
# break the object reference
self.obj_ref = None
def renew_subscription(self, lifetime):
if _debug: Subscription._debug("renew_subscription")
# suspend iff scheduled
if self.isScheduled:
self.suspend_task()
# reschedule the task if its not infinite
if lifetime != 0:
self.install_task(delta=lifetime)
def process_task(self):
if _debug: Subscription._debug("process_task")
# subscription is canceled
self.cancel_subscription()
#
# COVDetection
#
@bacpypes_debugging
class COVDetection(DetectionAlgorithm):
properties_tracked = ()
properties_reported = ()
monitored_property_reference = None
def __init__(self, obj):
if _debug: COVDetection._debug("__init__ %r", obj)
DetectionAlgorithm.__init__(self)
# keep track of the object
self.obj = obj
# build a list of parameters and matching object property references
kwargs = {}
for property_name in self.properties_tracked:
setattr(self, property_name, None)
kwargs[property_name] = (obj, property_name)
# let the base class set up the bindings and initial values
self.bind(**kwargs)
# list of all active subscriptions
self.cov_subscriptions = SubscriptionList()
def add_subscription(self, cov):
if _debug: COVDetection._debug("add_subscription %r", cov)
# add it to the subscription list for its object
self.cov_subscriptions.append(cov)
def cancel_subscription(self, cov):
if _debug: COVDetection._debug("cancel_subscription %r", cov)
# cancel the subscription timeout
if cov.isScheduled:
cov.suspend_task()
if _debug: COVDetection._debug(" - task suspended")
# remove it from the subscription list for its object
self.cov_subscriptions.remove(cov)
def execute(self):
if _debug: COVDetection._debug("execute")
# something changed, send out the notifications
self.send_cov_notifications()
def send_cov_notifications(self, subscription=None):
if _debug: COVDetection._debug("send_cov_notifications %r", subscription)
# check for subscriptions
if not len(self.cov_subscriptions):
return
# get the current time from the task manager
current_time = TaskManager().get_time()
if _debug: COVDetection._debug(" - current_time: %r", current_time)
# create a list of values
list_of_values = []
for property_name in self.properties_reported:
if _debug: COVDetection._debug(" - property_name: %r", property_name)
# get the class
property_datatype = self.obj.get_datatype(property_name)
if _debug: COVDetection._debug(" - property_datatype: %r", property_datatype)
# build the value
bundle_value = property_datatype(self.obj._values[property_name])
if _debug: COVDetection._debug(" - bundle_value: %r", bundle_value)
# bundle it into a sequence
property_value = PropertyValue(
propertyIdentifier=property_name,
value=Any(bundle_value),
)
# add it to the list
list_of_values.append(property_value)
if _debug: COVDetection._debug(" - list_of_values: %r", list_of_values)
# if the specific subscription was provided, that is the notification
# list, otherwise send it to all of them
if subscription is not None:
notification_list = [subscription]
else:
notification_list = self.cov_subscriptions
# loop through the subscriptions and send out notifications
for cov in notification_list:
if _debug: COVDetection._debug(" - cov: %s", repr(cov))
# calculate time remaining
if not cov.lifetime:
time_remaining = 0
else:
time_remaining = int(cov.taskTime - current_time)
# make sure it is at least one second
if not time_remaining:
time_remaining = 1
# build a request with the correct type
if cov.confirmed:
request = ConfirmedCOVNotificationRequest()
else:
request = UnconfirmedCOVNotificationRequest()
# fill in the parameters
request.pduDestination = cov.client_addr
request.subscriberProcessIdentifier = cov.proc_id
request.initiatingDeviceIdentifier = self.obj._app.localDevice.objectIdentifier
request.monitoredObjectIdentifier = cov.obj_id
request.timeRemaining = time_remaining
request.listOfValues = list_of_values
if _debug: COVDetection._debug(" - request: %s", repr(request))
# let the application send it
self.obj._app.cov_notification(cov, request)
def __str__(self):
return "<" + self.__class__.__name__ + \
"(" + ','.join(self.properties_tracked) + ')' + \
">"
class GenericCriteria(COVDetection):
properties_tracked = (
'presentValue',
'statusFlags',
)
properties_reported = (
'presentValue',
'statusFlags',
)
monitored_property_reference = 'presentValue'
@bacpypes_debugging
class COVIncrementCriteria(COVDetection):
properties_tracked = (
'presentValue',
'statusFlags',
'covIncrement',
)
properties_reported = (
'presentValue',
'statusFlags',
)
monitored_property_reference = 'presentValue'
def __init__(self, obj):
if _debug: COVIncrementCriteria._debug("__init__ %r", obj)
COVDetection.__init__(self, obj)
# previous reported value
self.previous_reported_value = None
@monitor_filter('presentValue')
def present_value_filter(self, old_value, new_value):
if _debug: COVIncrementCriteria._debug("present_value_filter %r %r", old_value, new_value)
# first time around initialize to the old value
if self.previous_reported_value is None:
if _debug: COVIncrementCriteria._debug(" - first value: %r", old_value)
self.previous_reported_value = old_value
# see if it changed enough to trigger reporting
value_changed = (new_value <= (self.previous_reported_value - self.obj.covIncrement)) \
or (new_value >= (self.previous_reported_value + self.obj.covIncrement))
if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed)
return value_changed
def send_cov_notifications(self, subscription=None):
if _debug: COVIncrementCriteria._debug("send_cov_notifications %r", subscription)
# when sending out notifications, keep the current value
self.previous_reported_value = self.presentValue
# continue
COVDetection.send_cov_notifications(self, subscription)
class AccessDoorCriteria(COVDetection):
properties_tracked = (
'presentValue',
'statusFlags',
'doorAlarmState',
)
properties_reported = (
'presentValue',
'statusFlags',
'doorAlarmState',
)
class AccessPointCriteria(COVDetection):
properties_tracked = (
'accessEventTime',
'statusFlags',
)
properties_reported = (
'accessEvent',
'statusFlags',
'accessEventTag',
'accessEventTime',
'accessEventCredential',
'accessEventAuthenticationFactor',
)
monitored_property_reference = 'accessEvent'
class CredentialDataInputCriteria(COVDetection):
properties_tracked = (
'updateTime',
'statusFlags'
)
properties_reported = (
'presentValue',
'statusFlags',
'updateTime',
)
class LoadControlCriteria(COVDetection):
properties_tracked = (
'presentValue',
'statusFlags',
'requestedShedLevel',
'startTime',
'shedDuration',
'dutyWindow',
)
properties_reported = (
'presentValue',
'statusFlags',
'requestedShedLevel',
'startTime',
'shedDuration',
'dutyWindow',
)
@bacpypes_debugging
class PulseConverterCriteria(COVIncrementCriteria):
properties_tracked = (
'presentValue',
'statusFlags',
'covPeriod',
)
properties_reported = (
'presentValue',
'statusFlags',
)
def __init__(self, obj):
if _debug: PulseConverterCriteria._debug("__init__ %r", obj)
COVIncrementCriteria.__init__(self, obj)
# check for a period
if self.covPeriod == 0:
if _debug: PulseConverterCriteria._debug(" - no periodic notifications")
self.cov_period_task = None
else:
if _debug: PulseConverterCriteria._debug(" - covPeriod: %r", self.covPeriod)
self.cov_period_task = RecurringFunctionTask(self.covPeriod * 1000, self.send_cov_notifications)
if _debug: PulseConverterCriteria._debug(" - cov period task created")
def add_subscription(self, cov):
if _debug: PulseConverterCriteria._debug("add_subscription %r", cov)
# let the parent classes do their thing
COVIncrementCriteria.add_subscription(self, cov)
# if there is a COV period task, install it
if self.cov_period_task:
self.cov_period_task.install_task()
if _debug: PulseConverterCriteria._debug(" - cov period task installed")
def cancel_subscription(self, cov):
if _debug: PulseConverterCriteria._debug("cancel_subscription %r", cov)
# let the parent classes do their thing
COVIncrementCriteria.cancel_subscription(self, cov)
# if there are no more subscriptions, cancel the task
if not len(self.cov_subscriptions):
if self.cov_period_task and self.cov_period_task.isScheduled:
self.cov_period_task.suspend_task()
if _debug: PulseConverterCriteria._debug(" - cov period task suspended")
self.cov_period_task = None
@monitor_filter('covPeriod')
def cov_period_filter(self, old_value, new_value):
if _debug: PulseConverterCriteria._debug("cov_period_filter %r %r", old_value, new_value)
# check for an old period
if old_value != 0:
if self.cov_period_task.isScheduled:
self.cov_period_task.suspend_task()
if _debug: PulseConverterCriteria._debug(" - canceled old task")
self.cov_period_task = None
# check for a new period
if new_value != 0:
self.cov_period_task = RecurringFunctionTask(new_value * 1000, self.cov_period_x)
self.cov_period_task.install_task()
if _debug: PulseConverterCriteria._debug(" - new task created and installed")
return False
def send_cov_notifications(self, subscription=None):
if _debug: PulseConverterCriteria._debug("send_cov_notifications %r", subscription)
# pass along to the parent class as if something changed
COVIncrementCriteria.send_cov_notifications(self, subscription)
# mapping from object type to appropriate criteria class
criteria_type_map = {
'accessPoint': AccessPointCriteria,
'analogInput': COVIncrementCriteria,
'analogOutput': COVIncrementCriteria,
'analogValue': COVIncrementCriteria,
'largeAnalogValue': COVIncrementCriteria,
'integerValue': COVIncrementCriteria,
'positiveIntegerValue': COVIncrementCriteria,
'lightingOutput': COVIncrementCriteria,
'binaryInput': GenericCriteria,
'binaryOutput': GenericCriteria,
'binaryValue': GenericCriteria,
'lifeSafetyPoint': GenericCriteria,
'lifeSafetyZone': GenericCriteria,
'multiStateInput': GenericCriteria,
'multiStateOutput': GenericCriteria,
'multiStateValue': GenericCriteria,
'octetString': GenericCriteria,
'characterString': GenericCriteria,
'timeValue': GenericCriteria,
'dateTimeValue': GenericCriteria,
'dateValue': GenericCriteria,
'timePatternValue': GenericCriteria,
'datePatternValue': GenericCriteria,
'dateTimePatternValue': GenericCriteria,
'credentialDataInput': CredentialDataInputCriteria,
'loadControl': LoadControlCriteria,
'pulseConverter': PulseConverterCriteria,
}
#
# ActiveCOVSubscriptions
#
@bacpypes_debugging
class ActiveCOVSubscriptions(Property):
def __init__(self):
Property.__init__(
self, 'activeCovSubscriptions', ListOf(COVSubscription),
default=None, optional=True, mutable=False,
)
def ReadProperty(self, obj, arrayIndex=None):
if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex)
# get the current time from the task manager
current_time = TaskManager().get_time()
if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time)
# start with an empty sequence
cov_subscriptions = ListOf(COVSubscription)()
# loop through the subscriptions
for cov in obj._app.subscriptions():
# calculate time remaining
if not cov.lifetime:
time_remaining = 0
else:
time_remaining = int(cov.taskTime - current_time)
# make sure it is at least one second
if not time_remaining:
time_remaining = 1
recipient = Recipient(
address=DeviceAddress(
networkNumber=cov.client_addr.addrNet or 0,
macAddress=cov.client_addr.addrAddr,
),
)
if _debug: ActiveCOVSubscriptions._debug(" - recipient: %r", recipient)
if _debug: ActiveCOVSubscriptions._debug(" - client MAC address: %r", cov.client_addr.addrAddr)
recipient_process = RecipientProcess(
recipient=recipient,
processIdentifier=cov.proc_id,
)
if _debug: ActiveCOVSubscriptions._debug(" - recipient_process: %r", recipient_process)
cov_subscription = COVSubscription(
recipient=recipient_process,
monitoredPropertyReference=ObjectPropertyReference(
objectIdentifier=cov.obj_id,
propertyIdentifier=cov_detection.monitored_property_reference,
),
issueConfirmedNotifications=cov.confirmed,
timeRemaining=time_remaining,
)
if hasattr(cov_detection, 'covIncrement'):
cov_subscription.covIncrement = cov_detection.covIncrement
if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription)
# add the list
cov_subscriptions.append(cov_subscription)
return cov_subscriptions
def WriteProperty(self, obj, value, arrayIndex=None, priority=None):
raise ExecutionError(errorClass='property', errorCode='writeAccessDenied')
#
# ChangeOfValueServices
#
@bacpypes_debugging
class ChangeOfValueServices(Capability):
def __init__(self):
if _debug: ChangeOfValueServices._debug("__init__")
Capability.__init__(self)
# map from an object to its detection algorithm
self.cov_detections = {}
# if there is a local device object, make sure it has an active COV
# subscriptions property
if self.localDevice and self.localDevice.activeCovSubscriptions is None:
self.localDevice.add_property(ActiveCOVSubscriptions())
def add_subscription(self, cov):
if _debug: ChangeOfValueServices._debug("add_subscription %r", cov)
# let the detection algorithm know this is a new or additional subscription
self.cov_detections[cov.obj_ref].add_subscription(cov)
def cancel_subscription(self, cov):
if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov)
# get the detection algorithm object
cov_detection = self.cov_detections[cov.obj_ref]
# let the detection algorithm know this subscription is going away
cov_detection.cancel_subscription(cov)
# if the detection algorithm doesn't have any subscriptions, remove it
if not len(cov_detection.cov_subscriptions):
if _debug: ChangeOfValueServices._debug(" - no more subscriptions")
# unbind all the hooks into the object
cov_detection.unbind()
# delete it from the object map
del self.cov_detections[cov.obj_ref]
def subscriptions(self):
"""Generator for the active subscriptions."""
if _debug: ChangeOfValueServices._debug("subscriptions")
# loop through the object and detection list
for obj, cov_detection in self.cov_detections.items():
for cov in cov_detection.cov_subscriptions:
yield cov
def cov_notification(self, cov, request):
if _debug: ChangeOfValueServices._debug("cov_notification %s %s", str(cov), str(request))
# create an IOCB with the request
iocb = IOCB(request)
if _debug: ChangeOfValueServices._debug(" - iocb: %r", iocb)
# add a callback for the response, even if it was unconfirmed
iocb.cov = cov
iocb.add_callback(self.cov_confirmation)
# send the request via the ApplicationIOController
self.request_io(iocb)
def cov_confirmation(self, iocb):
if _debug: ChangeOfValueServices._debug("cov_confirmation %r", iocb)
# do something for success
if iocb.ioResponse:
if _debug: ChangeOfValueServices._debug(" - ack")
self.cov_ack(iocb.cov, iocb.args[0], iocb.ioResponse)
elif isinstance(iocb.ioError, Error):
if _debug: ChangeOfValueServices._debug(" - error: %r", iocb.ioError.errorCode)
self.cov_error(iocb.cov, iocb.args[0], iocb.ioError)
elif isinstance(iocb.ioError, RejectPDU):
if _debug: ChangeOfValueServices._debug(" - reject: %r", iocb.ioError.apduAbortRejectReason)
self.cov_reject(iocb.cov, iocb.args[0], iocb.ioError)
elif isinstance(iocb.ioError, AbortPDU):
if _debug: ChangeOfValueServices._debug(" - abort: %r", iocb.ioError.apduAbortRejectReason)
self.cov_abort(iocb.cov, iocb.args[0], iocb.ioError)
def cov_ack(self, cov, request, response):
if _debug: ChangeOfValueServices._debug("cov_ack %r %r %r", cov, request, response)
def cov_error(self, cov, request, response):
if _debug: ChangeOfValueServices._debug("cov_error %r %r %r", cov, request, response)
def cov_reject(self, cov, request, response):
if _debug: ChangeOfValueServices._debug("cov_reject %r %r %r", cov, request, response)
def cov_abort(self, cov, request, response):
if _debug: ChangeOfValueServices._debug("cov_abort %r %r %r", cov, request, response)
### delete the rest of the pending requests for this client
def do_SubscribeCOVRequest(self, apdu):
if _debug: ChangeOfValueServices._debug("do_SubscribeCOVRequest %r", apdu)
# extract the pieces
client_addr = apdu.pduSource
proc_id = apdu.subscriberProcessIdentifier
obj_id = apdu.monitoredObjectIdentifier
confirmed = apdu.issueConfirmedNotifications
lifetime = apdu.lifetime
# request is to cancel the subscription
cancel_subscription = (confirmed is None) and (lifetime is None)
# find the object
obj = self.get_object_id(obj_id)
if _debug: ChangeOfValueServices._debug(" - object: %r", obj)
if not obj:
raise ExecutionError(errorClass='object', errorCode='unknownObject')
# look for an algorithm already associated with this object
cov_detection = self.cov_detections.get(obj, None)
# if there isn't one, make one and associate it with the object
if not cov_detection:
# look for an associated class and if it's not there it's not supported
criteria_class = criteria_type_map.get(obj_id[0], None)
if not criteria_class:
raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed')
# make one of these and bind it to the object
cov_detection = criteria_class(obj)
# keep track of it for other subscriptions
self.cov_detections[obj] = cov_detection
if _debug: ChangeOfValueServices._debug(" - cov_detection: %r", cov_detection)
# can a match be found?
cov = cov_detection.cov_subscriptions.find(client_addr, proc_id, obj_id)
if _debug: ChangeOfValueServices._debug(" - cov: %r", cov)
# if a match was found, update the subscription
if cov:
if cancel_subscription:
if _debug: ChangeOfValueServices._debug(" - cancel the subscription")
self.cancel_subscription(cov)
else:
if _debug: ChangeOfValueServices._debug(" - renew the subscription")
cov.renew_subscription(lifetime)
else:
if cancel_subscription:
if _debug: ChangeOfValueServices._debug(" - cancel a subscription that doesn't exist")
else:
if _debug: ChangeOfValueServices._debug(" - create a subscription")
# make a subscription
cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime)
if _debug: ChangeOfValueServices._debug(" - cov: %r", cov)
# add it to our subscriptions lists
self.add_subscription(cov)
# success
response = SimpleAckPDU(context=apdu)
# return the result
self.response(response)
# if the subscription is not being canceled, it is new or renewed,
# so send it a notification when you get a chance.
if not cancel_subscription:
if _debug: ChangeOfValueServices._debug(" - send a notification")
deferred(cov_detection.send_cov_notifications, cov)