Ejemplo n.º 1
0
        def setup_schedule(self):
            now = datetime.datetime.now()
            self._schedule_manager = ScheduleManager(
                preempt_grace_time,
                now=now,
                state_file_name=schedule_state_file)

            self.update_device_state_and_schedule(now)
Ejemplo n.º 2
0
    def _setup_schedule(self):
        now = utils.get_aware_utc_now()
        self._schedule_manager = ScheduleManager(
            self.preempt_grace_time,
            now=now,
            state_file_name=self.schedule_state_file)

        self._update_device_state_and_schedule(now)
Ejemplo n.º 3
0
    def _setup_schedule(self, preempt_grace_time, initial_state=None):

        try:
            now = self.volttime

            self._schedule_manager = ScheduleManager(
                preempt_grace_time,
                now=now,
                save_state_callback=self._schedule_save_callback,
                initial_state_string=initial_state)

            self._update_device_state_and_schedule(now)
        except:
            print "We don't have volttime yet, will setup the scheduler after we subscribe to volttime."
Ejemplo n.º 4
0
 def setup_schedule(self):
     now = datetime.datetime.now()
     self._schedule_manager = ScheduleManager(preempt_grace_time, now=now,
                                              state_file_name=schedule_state_file)
     
     self.update_device_state_and_schedule(now)
Ejemplo n.º 5
0
    class ActuatorAgent(Agent):
        '''Agent to listen for requests to talk to the sMAP driver.'''

        def __init__(self, **kwargs):
            super(ActuatorAgent, self).__init__(**kwargs)
            _log.debug("vip_identity: "+vip_identity)
            
            self._update_event = None
            self._device_states = {}
                    
        @RPC.export
        def heart_beat(self):
            _log.debug("sending heartbeat")
            self.vip.rpc.call(driver_vip_identity, 'heart_beat')
        
        @Core.receiver('onstart')
        def on_start(self, sender, **kwargs):
            self.setup_schedule()
            self.vip.pubsub.subscribe(peer='pubsub',
                                      prefix=topics.ACTUATOR_GET(), 
                                      callback=self.handle_get)

            self.vip.pubsub.subscribe(peer='pubsub',
                                      prefix=topics.ACTUATOR_SET(),
                                      callback=self.handle_set)
            
            self.vip.pubsub.subscribe(peer='pubsub',
                                      prefix=topics.ACTUATOR_SCHEDULE_REQUEST(), 
                                      callback=self.handle_schedule_request)
            
        
        def setup_schedule(self):
            now = datetime.datetime.now()
            self._schedule_manager = ScheduleManager(preempt_grace_time, now=now,
                                                     state_file_name=schedule_state_file)
            
            self.update_device_state_and_schedule(now)
            
        def update_device_state_and_schedule(self, now):
            _log.debug("update_device_state_and schedule")
            #Sanity check now.
            #This is specifically for when this is running in a VM that gets suspeded and then resumed.
            #If we don't make this check a resumed VM will publish one event per minute of 
            # time the VM was suspended for. 
            test_now = datetime.datetime.now()
            if test_now - now > datetime.timedelta(minutes=3):
                now = test_now
            
            self._device_states = self._schedule_manager.get_schedule_state(now)
            schedule_next_event_time = self._schedule_manager.get_next_event_time(now)
            new_update_event_time = self._get_ajusted_next_event_time(now, schedule_next_event_time)
            
            for device, state in self._device_states.iteritems():
                header = self.get_headers(state.agent_id, time=str(now), task_id=state.task_id)
                header['window'] = state.time_remaining
                topic = topics.ACTUATOR_SCHEDULE_ANNOUNCE_RAW.replace('{device}', device)
                self.vip.pubsub.publish('pubsub', topic, headers=header)
                
            if self._update_event is not None:
                #This won't hurt anything if we are canceling ourselves.
                self._update_event.cancel()
            self._update_event = self.core.schedule(new_update_event_time, 
                                                    self._update_schedule_state,
                                                    new_update_event_time)  
            
            
        def _get_ajusted_next_event_time(self, now, next_event_time):
            _log.debug("_get_adjusted_next_event_time")
            latest_next = now + datetime.timedelta(seconds=schedule_publish_interval)
            #Round to the next second to fix timer goofyness in agent timers.
            if latest_next.microsecond:
                latest_next = latest_next.replace(microsecond=0) + datetime.timedelta(seconds=1)
            if next_event_time is None or latest_next < next_event_time:
                return latest_next
            return next_event_time
        
            
        def _update_schedule_state(self, now):            
            self.update_device_state_and_schedule(now)
                            

        def handle_get(self, peer, sender, bus, topic, headers, message):
            point = topic.replace(topics.ACTUATOR_GET()+'/', '', 1)
            requester = headers.get('requesterID')
            headers = self.get_headers(requester)
            try:
                value = self.get_point(point)
                self.push_result_topic_pair(VALUE_RESPONSE_PREFIX,
                                            point, headers, value)
            except StandardError as ex:
                error = {'type': ex.__class__.__name__, 'value': str(ex)}
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                            point, headers, error)


        def handle_set(self, peer, sender, bus, topic, headers, message):
            point = topic.replace(topics.ACTUATOR_SET()+'/', '', 1)
            requester = headers.get('requesterID')
            headers = self.get_headers(requester)
            if not message:
                error = {'type': 'ValueError', 'value': 'missing argument'}
                _log.debug('ValueError: '+str(error))
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                            point, headers, error)
                return
            else:
                try:
                    message = message[0]
                    if isinstance(message, bool):
                        message = int(message)
                except ValueError as ex:
                    # Could be ValueError of JSONDecodeError depending
                    # on if simplesjson was used.  JSONDecodeError
                    # inherits from ValueError
                    _log.debug('ValueError: '+message)
                    error = {'type': 'ValueError', 'value': str(ex)}
                    self.push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                                point, headers, error)
                    return
            
            try:
                self.set_point(requester, point, message)
            except StandardError as ex:
                
                error = {'type': ex.__class__.__name__, 'value': str(ex)}
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                            point, headers, error)
                _log.debug('Actuator Agent Error: '+str(error))
                
                
        @RPC.export        
        def get_point(self, topic):
            topic = topic.strip('/')
            _log.debug('handle_get: {topic}'.format(topic=topic))
            path, point_name = topic.rsplit('/', 1)
            return self.vip.rpc.call(driver_vip_identity, 'get_point', path, point_name).get()
        
        @RPC.export
        def set_point(self, requester_id, topic, value):  
            topic = topic.strip('/')
            _log.debug('handle_set: {topic},{requester_id}, {value}'.
                       format(topic=topic, requester_id=requester_id, value=value))
            
            path, point_name = topic.rsplit('/', 1)
            
            headers = self.get_headers(requester_id)
            
            if self.check_lock(path, requester_id):
                result = self.vip.rpc.call(driver_vip_identity, 'set_point', path, point_name, value).get()
        
                headers = self.get_headers(requester_id)
                self.push_result_topic_pair(WRITE_ATTEMPT_PREFIX,
                                            topic, headers, value)
                self.push_result_topic_pair(VALUE_RESPONSE_PREFIX,
                                            topic, headers, result)
            else:
                raise LockError("caller does not have this lock")
                
            return result

        def check_lock(self, device, requester):
            _log.debug('check_lock: {device}, {requester}'.format(device=device, 
                                                                  requester=requester))
            device = device.strip('/')
            if device in self._device_states:
                device_state = self._device_states[device]
                return device_state.agent_id == requester
            return False


        def handle_schedule_request(self, peer, sender, bus, topic, headers, message):
            request_type = headers.get('type')
            _log.debug('handle_schedule_request: {topic}, {headers}, {message}'.
                       format(topic=topic, headers=str(headers), message=str(message)))
            
            requester_id = headers.get('requesterID')
            task_id = headers.get('taskID')
            priority = headers.get('priority')
                   
            if request_type == SCHEDULE_ACTION_NEW:
                try:
                    if len(message) == 1:
                        requests = message[0]
                    else:
                        requests = message
                
                    self.request_new_schedule(requester_id, task_id, priority, requests)
                except StandardError as ex:
                    _log.error('bad request: {request}, {error}'.format(request=requests, error=str(ex)))
                    self.vip.pubsub.publish('pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers,
                                            {'result':SCHEDULE_RESPONSE_FAILURE, 
                                             'data': {},
                                             'info': 'INVALID_REQUEST_TYPE'})
                    
            elif request_type == SCHEDULE_ACTION_CANCEL:
                try:
                    self.request_cancel_schedule(requester_id, task_id)
                except StandardError as ex:
                    _log.error('bad request: {request}, {error}'.format(request=requests, error=str(ex)))
                    self.vip.pubsub.publish('pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers,
                                            {'result':SCHEDULE_RESPONSE_FAILURE, 
                                             'data': {},
                                             'info': 'INVALID_REQUEST_TYPE'})
                
            else:
                _log.debug('handle-schedule_request, invalid request type')
                self.vip.pubsub.publish('pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers,
                                        {'result':SCHEDULE_RESPONSE_FAILURE, 
                                         'data': {},
                                         'info': 'INVALID_REQUEST_TYPE'})
            
        
        @RPC.export    
        def request_new_schedule(self, requester_id, task_id, priority, requests):
            now = datetime.datetime.now()
            
            if isinstance(requests[0], basestring):
                requests = [requests]
            
            requests = [[r[0].strip('/'),parse(r[1]),parse(r[2])] for r in requests]
                
            
            _log.debug("Got new schedule request: {}, {}, {}, {}".
                       format(requester_id, task_id, priority, requests))
            
            result = self._schedule_manager.request_slots(requester_id, task_id, requests, priority, now)
            success = SCHEDULE_RESPONSE_SUCCESS if result.success else SCHEDULE_RESPONSE_FAILURE
            
            #Dealing with success and other first world problems.
            if result.success:
                self.update_device_state_and_schedule(now)
                for preempted_task in result.data:
                    topic = topics.ACTUATOR_SCHEDULE_RESULT()
                    headers = self.get_headers(preempted_task[0], task_id=preempted_task[1])
                    headers['type'] = SCHEDULE_ACTION_CANCEL
                    self.vip.pubsub.publish('pubsub', topic, headers=headers, 
                                            message={'result':SCHEDULE_CANCEL_PREEMPTED,
                                                     'info': '',
                                                     'data':{'agentID': requester_id,
                                                             'taskID': task_id}})
            
            #If we are successful we do something else with the real result data
            data = result.data if not result.success else {}        
            topic = topics.ACTUATOR_SCHEDULE_RESULT()
            headers = self.get_headers(requester_id, task_id=task_id)
            headers['type'] = SCHEDULE_ACTION_NEW
            results = {'result':success, 
                       'data': data, 
                       'info':result.info_string}
            self.vip.pubsub.publish('pubsub', topic, headers=headers, message=results)
            
            return results
                    
        @RPC.export 
        def request_cancel_schedule(self, requester_id, task_id):
            now = datetime.datetime.now()
            headers = self.get_headers(requester_id, task_id=task_id)
            
            result = self._schedule_manager.cancel_task(requester_id, task_id, now)
            success = SCHEDULE_RESPONSE_SUCCESS if result.success else SCHEDULE_RESPONSE_FAILURE
            
            topic = topics.ACTUATOR_SCHEDULE_RESULT()
            message = {'result':success,  
                       'info': result.info_string,
                       'data':{}}
            self.vip.pubsub.publish('pubsub', topic, 
                                    headers=headers, 
                                    message=message)
            
            if result.success:
                self.update_device_state_and_schedule(now) 
                
            return message           
            

        def get_headers(self, requester, time=None, task_id=None):
            headers = {}
            if time is not None:
                headers['time'] = time
            else:
                headers = {'time': str(datetime.datetime.utcnow())}
            if requester is not None:
                headers['requesterID'] = requester
            if task_id is not None:
                headers['taskID'] = task_id
            return headers


        def push_result_topic_pair(self, prefix, point, headers, *args):
            topic = normtopic('/'.join([prefix, point]))
            self.vip.pubsub.publish('pubsub', topic, headers, message = args)
Ejemplo n.º 6
0
    class ActuatorAgent(Agent):
        '''Agent to listen for requests to talk to the sMAP driver.'''
        def __init__(self, **kwargs):
            super(ActuatorAgent, self).__init__(**kwargs)
            _log.debug("vip_identity: " + vip_identity)

            self._update_event = None
            self._device_states = {}

        @RPC.export
        def heart_beat(self):
            _log.debug("sending heartbeat")
            self.vip.rpc.call(driver_vip_identity, 'heart_beat')

        @Core.receiver('onstart')
        def on_start(self, sender, **kwargs):
            self.setup_schedule()
            self.vip.pubsub.subscribe(peer='pubsub',
                                      prefix=topics.ACTUATOR_GET(),
                                      callback=self.handle_get)

            self.vip.pubsub.subscribe(peer='pubsub',
                                      prefix=topics.ACTUATOR_SET(),
                                      callback=self.handle_set)

            self.vip.pubsub.subscribe(
                peer='pubsub',
                prefix=topics.ACTUATOR_SCHEDULE_REQUEST(),
                callback=self.handle_schedule_request)

        def setup_schedule(self):
            now = datetime.datetime.now()
            self._schedule_manager = ScheduleManager(
                preempt_grace_time,
                now=now,
                state_file_name=schedule_state_file)

            self.update_device_state_and_schedule(now)

        def update_device_state_and_schedule(self, now):
            _log.debug("update_device_state_and schedule")
            #Sanity check now.
            #This is specifically for when this is running in a VM that gets suspeded and then resumed.
            #If we don't make this check a resumed VM will publish one event per minute of
            # time the VM was suspended for.
            test_now = datetime.datetime.now()
            if test_now - now > datetime.timedelta(minutes=3):
                now = test_now

            self._device_states = self._schedule_manager.get_schedule_state(
                now)
            schedule_next_event_time = self._schedule_manager.get_next_event_time(
                now)
            new_update_event_time = self._get_ajusted_next_event_time(
                now, schedule_next_event_time)

            for device, state in self._device_states.iteritems():
                header = self.get_headers(state.agent_id,
                                          time=str(now),
                                          task_id=state.task_id)
                header['window'] = state.time_remaining
                topic = topics.ACTUATOR_SCHEDULE_ANNOUNCE_RAW.replace(
                    '{device}', device)
                self.vip.pubsub.publish('pubsub', topic, headers=header)

            if self._update_event is not None:
                #This won't hurt anything if we are canceling ourselves.
                self._update_event.cancel()
            self._update_event = self.core.schedule(
                new_update_event_time, self._update_schedule_state,
                new_update_event_time)

        def _get_ajusted_next_event_time(self, now, next_event_time):
            _log.debug("_get_adjusted_next_event_time")
            latest_next = now + datetime.timedelta(
                seconds=schedule_publish_interval)
            #Round to the next second to fix timer goofyness in agent timers.
            if latest_next.microsecond:
                latest_next = latest_next.replace(
                    microsecond=0) + datetime.timedelta(seconds=1)
            if next_event_time is None or latest_next < next_event_time:
                return latest_next
            return next_event_time

        def _update_schedule_state(self, now):
            self.update_device_state_and_schedule(now)

        def handle_get(self, peer, sender, bus, topic, headers, message):
            point = topic.replace(topics.ACTUATOR_GET() + '/', '', 1)
            requester = headers.get('requesterID')
            headers = self.get_headers(requester)
            try:
                value = self.get_point(point)
                self.push_result_topic_pair(VALUE_RESPONSE_PREFIX, point,
                                            headers, value)
            except StandardError as ex:
                error = {'type': ex.__class__.__name__, 'value': str(ex)}
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX, point,
                                            headers, error)

        def handle_set(self, peer, sender, bus, topic, headers, message):
            point = topic.replace(topics.ACTUATOR_SET() + '/', '', 1)
            requester = headers.get('requesterID')
            headers = self.get_headers(requester)
            if not message:
                error = {'type': 'ValueError', 'value': 'missing argument'}
                _log.debug('ValueError: ' + str(error))
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX, point,
                                            headers, error)
                return
            else:
                try:
                    message = message[0]
                    if isinstance(message, bool):
                        message = int(message)
                except ValueError as ex:
                    # Could be ValueError of JSONDecodeError depending
                    # on if simplesjson was used.  JSONDecodeError
                    # inherits from ValueError
                    _log.debug('ValueError: ' + message)
                    error = {'type': 'ValueError', 'value': str(ex)}
                    self.push_result_topic_pair(ERROR_RESPONSE_PREFIX, point,
                                                headers, error)
                    return

            try:
                self.set_point(requester, point, message)
            except StandardError as ex:

                error = {'type': ex.__class__.__name__, 'value': str(ex)}
                self.push_result_topic_pair(ERROR_RESPONSE_PREFIX, point,
                                            headers, error)
                _log.debug('Actuator Agent Error: ' + str(error))

        @RPC.export
        def get_point(self, topic):
            topic = topic.strip('/')
            _log.debug('handle_get: {topic}'.format(topic=topic))
            path, point_name = topic.rsplit('/', 1)
            return self.vip.rpc.call(driver_vip_identity, 'get_point', path,
                                     point_name).get()

        @RPC.export
        def set_point(self, requester_id, topic, value):
            topic = topic.strip('/')
            _log.debug('handle_set: {topic},{requester_id}, {value}'.format(
                topic=topic, requester_id=requester_id, value=value))

            path, point_name = topic.rsplit('/', 1)
            self.vip.rpc.call(driver_vip_identity, 'set_point', path,
                              point_name, value)

            headers = self.get_headers(requester_id)

            if self.check_lock(path, requester_id):
                result = self.vip.rpc.call(driver_vip_identity, 'set_point',
                                           path, point_name, value).get()

                headers = self.get_headers(requester_id)
                self.push_result_topic_pair(WRITE_ATTEMPT_PREFIX, topic,
                                            headers, value)
                self.push_result_topic_pair(VALUE_RESPONSE_PREFIX, topic,
                                            headers, result)
            else:
                raise LockError("caller does not have this lock")

            return result

        def check_lock(self, device, requester):
            _log.debug('check_lock: {device}, {requester}'.format(
                device=device, requester=requester))
            device = device.strip('/')
            if device in self._device_states:
                device_state = self._device_states[device]
                return device_state.agent_id == requester
            return False

        def handle_schedule_request(self, peer, sender, bus, topic, headers,
                                    message):
            request_type = headers.get('type')
            _log.debug(
                'handle_schedule_request: {topic}, {headers}, {message}'.
                format(topic=topic, headers=str(headers),
                       message=str(message)))

            requester_id = headers.get('requesterID')
            task_id = headers.get('taskID')
            priority = headers.get('priority')

            if request_type == SCHEDULE_ACTION_NEW:
                try:
                    if len(message) == 1:
                        requests = message[0]
                    else:
                        requests = message

                    self.request_new_schedule(requester_id, task_id, priority,
                                              requests)
                except StandardError as ex:
                    _log.error('bad request: {request}, {error}'.format(
                        request=requests, error=str(ex)))
                    self.vip.pubsub.publish(
                        'pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers, {
                            'result': SCHEDULE_RESPONSE_FAILURE,
                            'data': {},
                            'info': 'INVALID_REQUEST_TYPE'
                        })

            elif request_type == SCHEDULE_ACTION_CANCEL:
                try:
                    self.request_cancel_schedule(requester_id, task_id)
                except StandardError as ex:
                    _log.error('bad request: {request}, {error}'.format(
                        request=requests, error=str(ex)))
                    self.vip.pubsub.publish(
                        'pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers, {
                            'result': SCHEDULE_RESPONSE_FAILURE,
                            'data': {},
                            'info': 'INVALID_REQUEST_TYPE'
                        })

            else:
                _log.debug('handle-schedule_request, invalid request type')
                self.vip.pubsub.publish(
                    'pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers, {
                        'result': SCHEDULE_RESPONSE_FAILURE,
                        'data': {},
                        'info': 'INVALID_REQUEST_TYPE'
                    })

        @RPC.export
        def request_new_schedule(self, requester_id, task_id, priority,
                                 requests):
            now = datetime.datetime.now()

            if isinstance(requests[0], basestring):
                requests = [requests]

            requests = [[r[0].strip('/'),
                         parse(r[1]),
                         parse(r[2])] for r in requests]

            _log.debug("Got new schedule request: {}, {}, {}, {}".format(
                requester_id, task_id, priority, requests))

            result = self._schedule_manager.request_slots(
                requester_id, task_id, requests, priority, now)
            success = SCHEDULE_RESPONSE_SUCCESS if result.success else SCHEDULE_RESPONSE_FAILURE

            #Dealing with success and other first world problems.
            if result.success:
                self.update_device_state_and_schedule(now)
                for preempted_task in result.data:
                    topic = topics.ACTUATOR_SCHEDULE_RESULT()
                    headers = self.get_headers(preempted_task[0],
                                               task_id=preempted_task[1])
                    headers['type'] = SCHEDULE_ACTION_CANCEL
                    self.vip.pubsub.publish('pubsub',
                                            topic,
                                            headers=headers,
                                            message={
                                                'result':
                                                SCHEDULE_CANCEL_PREEMPTED,
                                                'info': '',
                                                'data': {
                                                    'agentID': requester_id,
                                                    'taskID': task_id
                                                }
                                            })

            #If we are successful we do something else with the real result data
            data = result.data if not result.success else {}
            topic = topics.ACTUATOR_SCHEDULE_RESULT()
            headers = self.get_headers(requester_id, task_id=task_id)
            headers['type'] = SCHEDULE_ACTION_NEW
            results = {
                'result': success,
                'data': data,
                'info': result.info_string
            }
            self.vip.pubsub.publish('pubsub',
                                    topic,
                                    headers=headers,
                                    message=results)

            return results

        @RPC.export
        def request_cancel_schedule(self, requester_id, task_id):
            now = datetime.datetime.now()
            headers = self.get_headers(requester_id, task_id=task_id)

            result = self._schedule_manager.cancel_task(
                requester_id, task_id, now)
            success = SCHEDULE_RESPONSE_SUCCESS if result.success else SCHEDULE_RESPONSE_FAILURE

            topic = topics.ACTUATOR_SCHEDULE_RESULT()
            message = {
                'result': success,
                'info': result.info_string,
                'data': {}
            }
            self.vip.pubsub.publish('pubsub',
                                    topic,
                                    headers=headers,
                                    message=message)

            if result.success:
                self.update_device_state_and_schedule(now)

            return message

        def get_headers(self, requester, time=None, task_id=None):
            headers = {}
            if time is not None:
                headers['time'] = time
            else:
                headers = {'time': str(datetime.datetime.utcnow())}
            if requester is not None:
                headers['requesterID'] = requester
            if task_id is not None:
                headers['taskID'] = task_id
            return headers

        def push_result_topic_pair(self, prefix, point, headers, *args):
            topic = normtopic('/'.join([prefix, point]))
            self.vip.pubsub.publish('pubsub', topic, headers, message=args)
Ejemplo n.º 7
0
class ActuatorAgent(Agent):
    """
    The Actuator Agent regulates control of devices by other agents. Agents
    request a schedule and then issue commands to the device through
    this agent.
    
    The Actuator Agent also sends out the signal to drivers to trigger
    a device heartbeat.
    
    :param heartbeat_interval: Interval in seonds to send out a heartbeat 
        to devices. 
    :param schedule_publish_interval: Interval in seonds to publish the
        currently active schedules. 
    :param schedule_state_file: Name of the file to save the current schedule
        state to. This file is updated every time a schedule changes. 
    :param preempt_grace_time: Time in seconds after a schedule is preemted
        before it is actually cancelled. 
    :param driver_vip_identity: VIP identity of the Master Driver Agent. 

    :type heartbeat_interval: float
    :type schedule_publish_interval: float
    :type preempt_grace_time: float
    :type schedule_state_file: str
    :type driver_vip_identity: str
    """
    def __init__(self, heartbeat_interval=60, schedule_publish_interval=60,
                 schedule_state_file=None, preempt_grace_time=60,
                 driver_vip_identity='platform.driver', **kwargs):
        
        super(ActuatorAgent, self).__init__(**kwargs)
        _log.debug("vip_identity: " + self.core.identity)

        self._update_event = None
        self._device_states = {}
        self.heartbeat_interval = heartbeat_interval
        self.schedule_publish_interval = schedule_publish_interval
        self.schedule_state_file = schedule_state_file
        self.preempt_grace_time = preempt_grace_time
        self.driver_vip_identity = driver_vip_identity
                
    def _heart_beat(self):
        _log.debug("sending heartbeat")
        try:
            self.vip.rpc.call(self.driver_vip_identity, 'heart_beat').get()
        except Unreachable:
            _log.warning("Master driver is not running")
        except Exception as e:
            _log.warning(''.join([e.__class__.__name__,'(',e.message,')']))

    @Core.receiver('onstart')
    def _on_start(self, sender, **kwargs):
        self._setup_schedule()
        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_GET(),
                                  callback=self.handle_get)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_SET(),
                                  callback=self.handle_set)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_SCHEDULE_REQUEST(),
                                  callback=self.handle_schedule_request)
        
        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_REVERT_POINT(),
                                  callback=self.handle_revert_point)
        
        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_REVERT_DEVICE(),
                                  callback=self.handle_revert_device)
        
        self.core.periodic(self.heartbeat_interval, self._heart_beat)

    def _setup_schedule(self):
        now = datetime.datetime.now()
        self._schedule_manager = ScheduleManager(self.preempt_grace_time, now=now,
                                                 state_file_name=self.schedule_state_file)

        self._update_device_state_and_schedule(now)

    def _update_device_state_and_schedule(self, now):
        _log.debug("_update_device_state_and_schedule")
        # Sanity check now.
        # This is specifically for when this is running in a VM that gets suspeded and then resumed.
        # If we don't make this check a resumed VM will publish one event per minute of
        # time the VM was suspended for. 
        test_now = datetime.datetime.now()
        if test_now - now > datetime.timedelta(minutes=3):
            now = test_now

        self._device_states = self._schedule_manager.get_schedule_state(now)
        schedule_next_event_time = self._schedule_manager.get_next_event_time(now)
        new_update_event_time = self._get_ajusted_next_event_time(now, schedule_next_event_time)

        for device, state in self._device_states.iteritems():
            header = self._get_headers(state.agent_id, time=utils.format_timestamp(now), task_id=state.task_id)
            header['window'] = state.time_remaining
            topic = topics.ACTUATOR_SCHEDULE_ANNOUNCE_RAW.replace('{device}', device)
            self.vip.pubsub.publish('pubsub', topic, headers=header)

        if self._update_event is not None:
            # This won't hurt anything if we are canceling ourselves.
            self._update_event.cancel()
        self._update_event = self.core.schedule(new_update_event_time,
                                                self._update_schedule_state,
                                                new_update_event_time)

    def _get_ajusted_next_event_time(self, now, next_event_time):
        _log.debug("_get_adjusted_next_event_time")
        latest_next = now + datetime.timedelta(seconds=self.schedule_publish_interval)
        # Round to the next second to fix timer goofyness in agent timers.
        if latest_next.microsecond:
            latest_next = latest_next.replace(microsecond=0) + datetime.timedelta(seconds=1)
        if next_event_time is None or latest_next < next_event_time:
            return latest_next
        return next_event_time

    def _update_schedule_state(self, now):
        self._update_device_state_and_schedule(now)

    def _handle_remote_error(self, ex, point, headers):
        try:
            exc_type = ex.exc_info['exc_type']
            exc_args = ex.exc_info['exc_args']
        except KeyError:
            exc_type = "RemoteError"
            exc_args = ex.message
        error = {'type': exc_type, 'value': str(exc_args)}
        self._push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                    point, headers, error)

        _log.debug('Actuator Agent Error: ' + str(error))

    def _handle_standard_error(self, ex, point, headers):
        error = {'type': ex.__class__.__name__, 'value': str(ex)}
        self._push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                    point, headers, error)
        _log.debug('Actuator Agent Error: ' + str(error))

    def handle_get(self, peer, sender, bus, topic, headers, message):
        """
        Requests up to date value of a point.
        
        To request a value publish a message to the following topic:

        ``devices/actuators/get/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/value/<full device path>/<actuation point>``
        
        with the message set to the value the point.
        
        """
        point = topic.replace(topics.ACTUATOR_GET() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        try:
            value = self.get_point(point)
            self._push_result_topic_pair(VALUE_RESPONSE_PREFIX,
                                        point, headers, value)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    def handle_set(self, peer, sender, bus, topic, headers, message):
        """
        Set the value of a point.
        
        To set a value publish a message to the following topic:

        ``devices/actuators/set/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/value/<full device path>/<actuation point>``
        
        with the message set to the value the point.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        
        """
        if sender == 'pubsub.compat':
            message = compat.unpack_legacy_message(headers, message)

        point = topic.replace(topics.ACTUATOR_SET() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        if not message:
            error = {'type': 'ValueError', 'value': 'missing argument'}
            _log.debug('ValueError: ' + str(error))
            self._push_result_topic_pair(ERROR_RESPONSE_PREFIX,
                                        point, headers, error)
            return

        try:
            self.set_point(requester, point, message)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    @RPC.export
    def get_point(self, topic, **kwargs):
        """
        RPC method
        
        Gets up to date value of a specific point on a device. 
        Does not require the device be scheduled. 
        
        :param topic: The topic of the point to grab in the 
                      format <device topic>/<point name>
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :returns: point value
        :rtype: any base python type"""
        topic = topic.strip('/')
        _log.debug('handle_get: {topic}'.format(topic=topic))
        path, point_name = topic.rsplit('/', 1)
        return self.vip.rpc.call(self.driver_vip_identity, 'get_point', path, point_name, **kwargs).get()

    @RPC.export
    def set_point(self, requester_id, topic, value, **kwargs):
        """RPC method
        
        Sets the value of a specific point on a device. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the point to set in the 
                      format <device topic>/<point name>
        :param value: Value to set point to.
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        :type value: any basic python type
        :returns: value point was actually set to. Usually invalid values 
                cause an error but some drivers (MODBUS) will return a different
                value with what the value was actually set to.
        :rtype: any base python type
        
        .. warning:: Calling without previously scheduling a device and not within 
                     the time allotted will raise a LockError"""
                     
        topic = topic.strip('/')
        _log.debug('handle_set: {topic},{requester_id}, {value}'.
                   format(topic=topic, requester_id=requester_id, value=value))

        path, point_name = topic.rsplit('/', 1)

        headers = self._get_headers(requester_id)
        if not isinstance(requester_id, str):
            raise TypeError("Agent id must be a nonempty string")
        if self._check_lock(path, requester_id):
            result = self.vip.rpc.call(self.driver_vip_identity, 'set_point', path, point_name, value, **kwargs).get()

            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(WRITE_ATTEMPT_PREFIX,
                                        topic, headers, value)
            self._push_result_topic_pair(VALUE_RESPONSE_PREFIX,
                                        topic, headers, result)
        else:
            raise LockError("caller ({}) does not have this lock".format(requester_id))

        return result
    
    def handle_revert_point(self, peer, sender, bus, topic, headers, message):
        """
        Revert the value of a point.
        
        To revert a value publish a message to the following topic:

        ``actuators/revert/point/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on

        ``devices/actuators/reverted/point/<full device path>/<actuation point>``
        
        This is to indicate that a point was reverted.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        """
        point = topic.replace(topics.ACTUATOR_REVERT_POINT()+'/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        
        try:
            self.revert_point(requester, point)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)
            
    def handle_revert_device(self, peer, sender, bus, topic, headers, message):
        """
        Revert all the writable values on a device.
        
        To revert a device publish a message to the following topic:

        ``devices/actuators/revert/device/<device path>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/reverted/device/<full device path>``
        
        to indicate that a point was reverted.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        """
        point = topic.replace(topics.ACTUATOR_REVERT_DEVICE()+'/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        
        try:
            self.revert_device(requester, point)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)
    
    @RPC.export
    def revert_point(self, requester_id, topic, **kwargs):  
        """
        RPC method
        
        Reverts the value of a specific point on a device to a default state. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the point to revert in the 
                      format <device topic>/<point name>
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        
        .. warning:: Calling without previously scheduling a device and not within 
                     the time allotted will raise a LockError"""
                     
        topic = topic.strip('/')
        _log.debug('handle_revert: {topic},{requester_id}'.
                   format(topic=topic, requester_id=requester_id))
        
        path, point_name = topic.rsplit('/', 1)
        
        headers = self._get_headers(requester_id)
        
        if self._check_lock(path, requester_id):
            self.vip.rpc.call(self.driver_vip_identity, 'revert_point', path, point_name, **kwargs).get()
    
            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(REVERT_POINT_RESPONSE_PREFIX,
                                        topic, headers, None)
        else:
            raise LockError("caller does not have this lock")
        
    @RPC.export
    def revert_device(self, requester_id, topic, **kwargs):  
        """
        RPC method
        
        Reverts all points on a device to a default state. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the device to revert
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        
        .. warning:: Calling without previously scheduling a device and not within 
                     the time allotted will raise a LockError"""
                     
        topic = topic.strip('/')
        _log.debug('handle_revert: {topic},{requester_id}'.
                   format(topic=topic, requester_id=requester_id))
        
        path = topic
        
        headers = self._get_headers(requester_id)
        
        if self._check_lock(path, requester_id):
            self.vip.rpc.call(self.driver_vip_identity, 'revert_device', path, **kwargs).get()
    
            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(REVERT_DEVICE_RESPONSE_PREFIX,
                                        topic, headers, None)
        else:
            raise LockError("caller does not have this lock")

    def _check_lock(self, device, requester):
        _log.debug('_check_lock: {device}, {requester}'.format(device=device,
                                                              requester=requester))
        device = device.strip('/')
        if device in self._device_states:
            device_state = self._device_states[device]
            return device_state.agent_id == requester
        return False

    def handle_schedule_request(self, peer, sender, bus, topic, headers, message):
        """        
        Schedule request pub/sub handler
        
        An agent can request a task schedule by publishing to the
        ``devices/actuators/schedule/request`` topic with the following header:
        
        .. code-block:: python
        
            {
                'type': 'NEW_SCHEDULE',
                'requesterID': <Agent ID>, #The name of the requesting agent.
                'taskID': <unique task ID>, #The desired task ID for this task. It must be unique among all other scheduled tasks.
                'priority': <task priority>, #The desired task priority, must be 'HIGH', 'LOW', or 'LOW_PREEMPT'
            }
            
        The message must describe the blocks of time using the format described in `Device Schedule`_. 
            
        A task may be canceled by publishing to the
        ``devices/actuators/schedule/request`` topic with the following header:
        
        .. code-block:: python
        
            {
                'type': 'CANCEL_SCHEDULE',
                'requesterID': <Agent ID>, #The name of the requesting agent.
                'taskID': <unique task ID>, #The task ID for the canceled Task.
            }
            
        requesterID
            The name of the requesting agent.
        taskID
            The desired task ID for this task. It must be unique among all other scheduled tasks.
        priority
            The desired task priority, must be 'HIGH', 'LOW', or 'LOW_PREEMPT'
            
        No message is requires to cancel a schedule.
            
        """
        if sender == 'pubsub.compat':
            message = compat.unpack_legacy_message(headers, message)

        request_type = headers.get('type')
        _log.debug('handle_schedule_request: {topic}, {headers}, {message}'.
                   format(topic=topic, headers=str(headers), message=str(message)))

        requester_id = headers.get('requesterID')
        task_id = headers.get('taskID')
        priority = headers.get('priority')

        if request_type == SCHEDULE_ACTION_NEW:
            try:
                if len(message) == 1:
                    requests = message[0]
                else:
                    requests = message

                self.request_new_schedule(requester_id, task_id, priority, requests)
            except StandardError as ex:
                return self._handle_unknown_schedule_error(ex, headers, message)

        elif request_type == SCHEDULE_ACTION_CANCEL:
            try:
                self.request_cancel_schedule(requester_id, task_id)
            except StandardError as ex:
Ejemplo n.º 8
0
class ActuatorAgent(Agent):
    """
    The Actuator Agent regulates control of devices by other agents. Agents
    request a schedule and then issue commands to the device through
    this agent.
    
    The Actuator Agent also sends out the signal to drivers to trigger
    a device heartbeat.
    
    :param heartbeat_interval: Interval in seonds to send out a heartbeat 
        to devices. 
    :param schedule_publish_interval: Interval in seonds to publish the
        currently active schedules. 
    :param schedule_state_file: Name of the file to save the current schedule
        state to. This file is updated every time a schedule changes. 
    :param preempt_grace_time: Time in seconds after a schedule is preemted
        before it is actually cancelled. 
    :param driver_vip_identity: VIP identity of the Master Driver Agent. 

    :type heartbeat_interval: float
    :type schedule_publish_interval: float
    :type preempt_grace_time: float
    :type schedule_state_file: str
    :type driver_vip_identity: str
    """
    def __init__(self,
                 heartbeat_interval=60,
                 schedule_publish_interval=60,
                 schedule_state_file=None,
                 preempt_grace_time=60,
                 driver_vip_identity='platform.driver',
                 **kwargs):

        super(ActuatorAgent, self).__init__(**kwargs)
        _log.debug("vip_identity: " + self.core.identity)

        self._update_event = None
        self._device_states = {}
        self.heartbeat_interval = heartbeat_interval
        self.schedule_publish_interval = schedule_publish_interval
        self.schedule_state_file = schedule_state_file
        self.preempt_grace_time = preempt_grace_time
        self.driver_vip_identity = driver_vip_identity

    def _heart_beat(self):
        _log.debug("sending heartbeat")
        try:
            self.vip.rpc.call(self.driver_vip_identity,
                              'heart_beat').get(timeout=5.0)
        except Unreachable:
            _log.warning("Master driver is not running")
        except Exception as e:
            _log.warning(''.join([e.__class__.__name__, '(', e.message, ')']))

    @Core.receiver('onstart')
    def _on_start(self, sender, **kwargs):
        self._setup_schedule()
        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_GET(),
                                  callback=self.handle_get)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_SET(),
                                  callback=self.handle_set)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_SCHEDULE_REQUEST(),
                                  callback=self.handle_schedule_request)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_REVERT_POINT(),
                                  callback=self.handle_revert_point)

        self.vip.pubsub.subscribe(peer='pubsub',
                                  prefix=topics.ACTUATOR_REVERT_DEVICE(),
                                  callback=self.handle_revert_device)

        self.core.periodic(self.heartbeat_interval, self._heart_beat)

    def _setup_schedule(self):
        now = utils.get_aware_utc_now()
        self._schedule_manager = ScheduleManager(
            self.preempt_grace_time,
            now=now,
            state_file_name=self.schedule_state_file)

        self._update_device_state_and_schedule(now)

    def _update_device_state_and_schedule(self, now):
        _log.debug("_update_device_state_and_schedule")
        # Sanity check now.
        # This is specifically for when this is running in a VM that gets
        # suspeded and then resumed.
        # If we don't make this check a resumed VM will publish one event
        # per minute of
        # time the VM was suspended for.
        test_now = utils.get_aware_utc_now()
        if test_now - now > datetime.timedelta(minutes=3):
            now = test_now
        _log.debug(
            "In _update_device_state_and_schedule: now is {}".format(now))
        self._device_states = self._schedule_manager.get_schedule_state(now)
        _log.debug("device states is {}".format(self._device_states))
        schedule_next_event_time = self._schedule_manager.get_next_event_time(
            now)
        _log.debug(
            "schedule_next_event_time is {}".format(schedule_next_event_time))
        new_update_event_time = self._get_ajusted_next_event_time(
            now, schedule_next_event_time)
        _log.debug("new_update_event_time is {}".format(new_update_event_time))
        for device, state in self._device_states.iteritems():
            _log.debug("device, state -  {}, {}".format(device, state))
            header = self._get_headers(state.agent_id,
                                       time=utils.format_timestamp(now),
                                       task_id=state.task_id)
            header['window'] = state.time_remaining
            topic = topics.ACTUATOR_SCHEDULE_ANNOUNCE_RAW.replace(
                '{device}', device)
            self.vip.pubsub.publish('pubsub', topic, headers=header)

        if self._update_event is not None:
            # This won't hurt anything if we are canceling ourselves.
            self._update_event.cancel()
        self._update_event = self.core.schedule(new_update_event_time,
                                                self._update_schedule_state,
                                                new_update_event_time)

    def _get_ajusted_next_event_time(self, now, next_event_time):
        _log.debug("_get_adjusted_next_event_time")
        latest_next = now + datetime.timedelta(
            seconds=self.schedule_publish_interval)
        # Round to the next second to fix timer goofyness in agent timers.
        if latest_next.microsecond:
            latest_next = latest_next.replace(
                microsecond=0) + datetime.timedelta(seconds=1)
        if next_event_time is None or latest_next < next_event_time:
            return latest_next
        return next_event_time

    def _update_schedule_state(self, now):
        self._update_device_state_and_schedule(now)

    def _handle_remote_error(self, ex, point, headers):
        try:
            exc_type = ex.exc_info['exc_type']
            exc_args = ex.exc_info['exc_args']
        except KeyError:
            exc_type = "RemoteError"
            exc_args = ex.message
        error = {'type': exc_type, 'value': str(exc_args)}
        self._push_result_topic_pair(ERROR_RESPONSE_PREFIX, point, headers,
                                     error)

        _log.debug('Actuator Agent Error: ' + str(error))

    def _handle_standard_error(self, ex, point, headers):
        error = {'type': ex.__class__.__name__, 'value': str(ex)}
        self._push_result_topic_pair(ERROR_RESPONSE_PREFIX, point, headers,
                                     error)
        _log.debug('Actuator Agent Error: ' + str(error))

    def handle_get(self, peer, sender, bus, topic, headers, message):
        """
        Requests up to date value of a point.
        
        To request a value publish a message to the following topic:

        ``devices/actuators/get/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/value/<full device path>/<actuation point>``
        
        with the message set to the value the point.
        
        """
        point = topic.replace(topics.ACTUATOR_GET() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        try:
            value = self.get_point(point)
            self._push_result_topic_pair(VALUE_RESPONSE_PREFIX, point, headers,
                                         value)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    def handle_set(self, peer, sender, bus, topic, headers, message):
        """
        Set the value of a point.
        
        To set a value publish a message to the following topic:

        ``devices/actuators/set/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/value/<full device path>/<actuation point>``
        
        with the message set to the value the point.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        
        """
        if sender == 'pubsub.compat':
            message = compat.unpack_legacy_message(headers, message)

        point = topic.replace(topics.ACTUATOR_SET() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)
        if not message:
            error = {'type': 'ValueError', 'value': 'missing argument'}
            _log.debug('ValueError: ' + str(error))
            self._push_result_topic_pair(ERROR_RESPONSE_PREFIX, point, headers,
                                         error)
            return

        try:
            self.set_point(requester, point, message)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    @RPC.export
    def get_point(self, topic, **kwargs):
        """
        RPC method
        
        Gets up to date value of a specific point on a device. 
        Does not require the device be scheduled. 
        
        :param topic: The topic of the point to grab in the 
                      format <device topic>/<point name>
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :returns: point value
        :rtype: any base python type"""
        topic = topic.strip('/')
        _log.debug('handle_get: {topic}'.format(topic=topic))
        path, point_name = topic.rsplit('/', 1)
        return self.vip.rpc.call(self.driver_vip_identity, 'get_point', path,
                                 point_name, **kwargs).get()

    @RPC.export
    def set_point(self, requester_id, topic, value, **kwargs):
        """RPC method
        
        Sets the value of a specific point on a device. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the point to set in the 
                      format <device topic>/<point name>
        :param value: Value to set point to.
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        :type value: any basic python type
        :returns: value point was actually set to. Usually invalid values 
                cause an error but some drivers (MODBUS) will return a
                different
                value with what the value was actually set to.
        :rtype: any base python type
        
        .. warning:: Calling without previously scheduling a device and not
        within
                     the time allotted will raise a LockError"""

        topic = topic.strip('/')
        _log.debug('handle_set: {topic},{requester_id}, {value}'.format(
            topic=topic, requester_id=requester_id, value=value))

        path, point_name = topic.rsplit('/', 1)

        headers = self._get_headers(requester_id)
        if not isinstance(requester_id, str):
            raise TypeError("Agent id must be a nonempty string")
        if self._check_lock(path, requester_id):
            result = self.vip.rpc.call(self.driver_vip_identity, 'set_point',
                                       path, point_name, value,
                                       **kwargs).get()

            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(WRITE_ATTEMPT_PREFIX, topic, headers,
                                         value)
            self._push_result_topic_pair(VALUE_RESPONSE_PREFIX, topic, headers,
                                         result)
        else:
            raise LockError(
                "caller ({}) does not have this lock".format(requester_id))

        return result

    @RPC.export
    def set_multiple_points(self, requester_id, topics_values, **kwargs):
        """RPC method

        Set multiple points on multiple devices. Makes a single
        RPC call to the master driver per device.

        :param requester_id: Identifier given when requesting schedule.
        :param topics_values: List of (topic, value) tuples
        :param \*\*kwargs: Any driver specific parameters

        :returns: Dictionary of points to exceptions raised.
                  If all points were set successfully an empty
                  dictionary will be returned.

        .. warning:: calling without previously scheduling *all* devices
                     and not within the time allotted will raise a LockError
        """

        devices = collections.defaultdict(list)
        for topic, value in topics_values:
            topic = topic.strip('/')
            device, point_name = topic.rsplit('/', 1)
            devices[device].append((point_name, value))

        for device in devices:
            if not self._check_lock(device, requester_id):
                raise LockError(
                    "caller ({}) does not lock for device {}".format(
                        requester_id, device))

        results = {}
        for device, point_names_values in devices.iteritems():
            r = self.vip.rpc.call(self.driver_vip_identity,
                                  'set_multiple_points', device,
                                  point_names_values, **kwargs).get()
            results.update(r)

        return results

    def handle_revert_point(self, peer, sender, bus, topic, headers, message):
        """
        Revert the value of a point.
        
        To revert a value publish a message to the following topic:

        ``actuators/revert/point/<device path>/<actuation point>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on

        ``devices/actuators/reverted/point/<full device path>/<actuation
        point>``
        
        This is to indicate that a point was reverted.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        """
        point = topic.replace(topics.ACTUATOR_REVERT_POINT() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)

        try:
            self.revert_point(requester, point)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    def handle_revert_device(self, peer, sender, bus, topic, headers, message):
        """
        Revert all the writable values on a device.
        
        To revert a device publish a message to the following topic:

        ``devices/actuators/revert/device/<device path>``
        
        with the fallowing header:
        
        .. code-block:: python
        
            {
                'requesterID': <Agent ID>
            }
        
        The ActuatorAgent will reply on the **value** topic 
        for the actuator:

        ``devices/actuators/reverted/device/<full device path>``
        
        to indicate that a point was reverted.
        
        Errors will be published on 
        
        ``devices/actuators/error/<full device path>/<actuation point>``
        
        with the same header as the request.
        """
        point = topic.replace(topics.ACTUATOR_REVERT_DEVICE() + '/', '', 1)
        requester = headers.get('requesterID')
        headers = self._get_headers(requester)

        try:
            self.revert_device(requester, point)
        except RemoteError as ex:
            self._handle_remote_error(ex, point, headers)
        except StandardError as ex:
            self._handle_standard_error(ex, point, headers)

    @RPC.export
    def revert_point(self, requester_id, topic, **kwargs):
        """
        RPC method
        
        Reverts the value of a specific point on a device to a default state. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the point to revert in the 
                      format <device topic>/<point name>
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        
        .. warning:: Calling without previously scheduling a device and not
        within
                     the time allotted will raise a LockError"""

        topic = topic.strip('/')
        _log.debug('handle_revert: {topic},{requester_id}'.format(
            topic=topic, requester_id=requester_id))

        path, point_name = topic.rsplit('/', 1)

        headers = self._get_headers(requester_id)

        if self._check_lock(path, requester_id):
            self.vip.rpc.call(self.driver_vip_identity, 'revert_point', path,
                              point_name, **kwargs).get()

            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(REVERT_POINT_RESPONSE_PREFIX, topic,
                                         headers, None)
        else:
            raise LockError("caller does not have this lock")

    @RPC.export
    def revert_device(self, requester_id, topic, **kwargs):
        """
        RPC method
        
        Reverts all points on a device to a default state. 
        Requires the device be scheduled by the calling agent.
        
        :param requester_id: Identifier given when requesting schedule. 
        :param topic: The topic of the device to revert
        :param \*\*kwargs: Any driver specific parameters
        :type topic: str
        :type requester_id: str
        
        .. warning:: Calling without previously scheduling a device and not
        within
                     the time allotted will raise a LockError"""

        topic = topic.strip('/')
        _log.debug('handle_revert: {topic},{requester_id}'.format(
            topic=topic, requester_id=requester_id))

        path = topic

        headers = self._get_headers(requester_id)

        if self._check_lock(path, requester_id):
            self.vip.rpc.call(self.driver_vip_identity, 'revert_device', path,
                              **kwargs).get()

            headers = self._get_headers(requester_id)
            self._push_result_topic_pair(REVERT_DEVICE_RESPONSE_PREFIX, topic,
                                         headers, None)
        else:
            raise LockError("caller does not have this lock")

    def _check_lock(self, device, requester):
        _log.debug('_check_lock: {device}, {requester}'.format(
            device=device, requester=requester))
        device = device.strip('/')
        if device in self._device_states:
            device_state = self._device_states[device]
            return device_state.agent_id == requester
        return False

    def handle_schedule_request(self, peer, sender, bus, topic, headers,
                                message):
        """        
        Schedule request pub/sub handler
        
        An agent can request a task schedule by publishing to the
        ``devices/actuators/schedule/request`` topic with the following header:
        
        .. code-block:: python
        
            {
                'type': 'NEW_SCHEDULE',
                'requesterID': <Agent ID>, #The name of the requesting agent.
                'taskID': <unique task ID>, #The desired task ID for this
                task. It must be unique among all other scheduled tasks.
                'priority': <task priority>, #The desired task priority,
                must be 'HIGH', 'LOW', or 'LOW_PREEMPT'
            }
            
        The message must describe the blocks of time using the format
        described in `Device Schedule`_.
            
        A task may be canceled by publishing to the
        ``devices/actuators/schedule/request`` topic with the following header:
        
        .. code-block:: python
        
            {
                'type': 'CANCEL_SCHEDULE',
                'requesterID': <Agent ID>, #The name of the requesting agent.
                'taskID': <unique task ID>, #The task ID for the canceled Task.
            }
            
        requesterID
            The name of the requesting agent.
        taskID
            The desired task ID for this task. It must be unique among all
            other scheduled tasks.
        priority
            The desired task priority, must be 'HIGH', 'LOW', or 'LOW_PREEMPT'
            
        No message is requires to cancel a schedule.
            
        """
        if sender == 'pubsub.compat':
            message = compat.unpack_legacy_message(headers, message)

        request_type = headers.get('type')
        _log.debug(
            'handle_schedule_request: {topic}, {headers}, {message}'.format(
                topic=topic, headers=str(headers), message=str(message)))

        requester_id = headers.get('requesterID')
        task_id = headers.get('taskID')
        priority = headers.get('priority')

        if request_type == SCHEDULE_ACTION_NEW:
            try:
                if len(message) == 1:
                    requests = message[0]
                else:
                    requests = message

                self.request_new_schedule(requester_id, task_id, priority,
                                          requests)
            except StandardError as ex:
                return self._handle_unknown_schedule_error(
                    ex, headers, message)

        elif request_type == SCHEDULE_ACTION_CANCEL:
            try:
                self.request_cancel_schedule(requester_id, task_id)
            except StandardError as ex:
                return self._handle_unknown_schedule_error(
                    ex, headers, message)
        else:
            _log.debug('handle-schedule_request, invalid request type')
            self.vip.pubsub.publish(
                'pubsub', topics.ACTUATOR_SCHEDULE_RESULT(), headers, {
                    'result': SCHEDULE_RESPONSE_FAILURE,
                    'data': {},
                    'info': 'INVALID_REQUEST_TYPE'
                })

    @RPC.export
    def request_new_schedule(self, requester_id, task_id, priority, requests):
        """
        RPC method
        
        Requests one or more blocks on time on one or more device.
        
        :param requester_id: Requester name. 
        :param task_id: Task name.
        :param priority: Priority of the task. Must be either "HIGH", "LOW",
        or "LOW_PREEMPT"
        :param requests: A list of time slot requests in the format
        described in `Device Schedule`_.
        
        :type requester_id: str
        :type task_id: str
        :type priority: str
        :returns: Request result
        :rtype: dict       
        
        :Return Values:
        
            The return values are described in `New Task Response`_.
        """

        now = utils.get_aware_utc_now()

        topic = topics.ACTUATOR_SCHEDULE_RESULT()
        headers = self._get_headers(requester_id, task_id=task_id)
        headers['type'] = SCHEDULE_ACTION_NEW
        local_tz = get_localzone()
        try:
            if requests and isinstance(requests[0], basestring):
                requests = [requests]

            tmp_requests = requests
            requests = []
            for r in tmp_requests:
                device, start, end = r

                device = device.strip('/')
                start = utils.parse_timestamp_string(start)
                end = utils.parse_timestamp_string(end)

                if start.tzinfo is None:
                    start = local_tz.localize(start)
                if end.tzinfo is None:
                    end = local_tz.localize(end)

                requests.append([device, start, end])

        except StandardError as ex:
            return self._handle_unknown_schedule_error(ex, headers, requests)

        _log.debug("Got new schedule request: {}, {}, {}, {}".format(
            requester_id, task_id, priority, requests))

        result = self._schedule_manager.request_slots(requester_id, task_id,
                                                      requests, priority, now)
        success = SCHEDULE_RESPONSE_SUCCESS if result.success else \
            SCHEDULE_RESPONSE_FAILURE

        # Dealing with success and other first world problems.
        if result.success:
            self._update_device_state_and_schedule(now)
            for preempted_task in result.data:
                preempt_headers = self._get_headers(preempted_task[0],
                                                    task_id=preempted_task[1])
                preempt_headers['type'] = SCHEDULE_ACTION_CANCEL
                self.vip.pubsub.publish('pubsub',
                                        topic,
                                        headers=preempt_headers,
                                        message={
                                            'result':
                                            SCHEDULE_CANCEL_PREEMPTED,
                                            'info': '',
                                            'data': {
                                                'agentID': requester_id,
                                                'taskID': task_id
                                            }
                                        })

        # If we are successful we do something else with the real result data
        data = result.data if not result.success else {}

        results = {'result': success, 'data': data, 'info': result.info_string}
        self.vip.pubsub.publish('pubsub',
                                topic,
                                headers=headers,
                                message=results)

        return results

    def _handle_unknown_schedule_error(self, ex, headers, message):
        _log.error('bad request: {header}, {request}, {error}'.format(
            header=headers, request=message, error=str(ex)))
        results = {
            'result': "FAILURE",
            'data': {},
            'info':
            'MALFORMED_REQUEST: ' + ex.__class__.__name__ + ': ' + str(ex)
        }
        self.vip.pubsub.publish('pubsub',
                                topics.ACTUATOR_SCHEDULE_RESULT(),
                                headers=headers,
                                message=results)
        return results

    @RPC.export
    def request_cancel_schedule(self, requester_id, task_id):
        """RPC method
        
        Requests the cancelation of the specified task id.
        
        :param requester_id: Requester name. 
        :param task_id: Task name.
        
        :type requester_id: str
        :type task_id: str
        :returns: Request result
        :rtype: dict
        
        :Return Values: 

        The return values are described in `Cancel Task Response`_.
        
        """
        now = utils.get_aware_utc_now()
        headers = self._get_headers(requester_id, task_id=task_id)
        headers['type'] = SCHEDULE_ACTION_CANCEL

        result = self._schedule_manager.cancel_task(requester_id, task_id, now)
        success = SCHEDULE_RESPONSE_SUCCESS if result.success else \
            SCHEDULE_RESPONSE_FAILURE

        topic = topics.ACTUATOR_SCHEDULE_RESULT()
        message = {'result': success, 'info': result.info_string, 'data': {}}
        self.vip.pubsub.publish('pubsub',
                                topic,
                                headers=headers,
                                message=message)

        if result.success:
            self._update_device_state_and_schedule(now)

        return message

    def _get_headers(self, requester, time=None, task_id=None):
        headers = {}
        if time is not None:
            headers['time'] = time
        else:
            utcnow = utils.get_aware_utc_now()
            headers = {'time': utils.format_timestamp(utcnow)}
        if requester is not None:
            headers['requesterID'] = requester
        if task_id is not None:
            headers['taskID'] = task_id
        return headers

    def _push_result_topic_pair(self, prefix, point, headers, value):
        topic = normtopic('/'.join([prefix, point]))
        self.vip.pubsub.publish('pubsub', topic, headers, message=value)