def _control_event_loop(self): ''' This is the threading loop to perform control based on current oadr events Note the current implementation simply loops based on CONTROL_LOOP_INTERVAL except when an updated event is received by a VTN. ''' while not self._exit.is_set(): try: logger.debug("Updating control states...") events = self.event_handler.get_active_events() new_signal_level = self._update_control(events) logger.debug("Highest signal level is: %f", new_signal_level) changed = self._update_signal_level(new_signal_level) if changed: logger.debug("Updated current signal level!") except Exception as ex: logger.exception("Control loop error: %s", ex) self._control_loop_signal.wait(self.control_loop_interval) self._control_loop_signal.clear( ) # in case it was triggered by a poll update logger.info("Control loop exiting.")
def __init__(self, event_config, control_opts={}, client_id=None): ''' base class initializer, creates an `event.EventHandler` as `self.event_handler` and a `control.EventController` as `self.event_controller event_config -- A dictionary containing keyword arugments for the EventHandler control_opts -- a dict of opts for `control.EventController` init ''' # Get an EventHandler and an EventController self.event_handler = event.EventHandler(**event_config) self.event_controller = controller.EventController( self.event_handler, **control_opts) # Add an exit thread for the module self._exit = threading.Event() self._exit.clear() logger.addFilter(IdFilter(client_id=client_id)) logger.info('Created base handler.') pass
def exit(self): ''' Shutdown the base handler and its threads. ''' self.event_controller.exit() # Stop the event controller self._exit.set() logger.info('Shutdown base handler.')
def __init__( self, event_config, vtn_base_uri, control_opts={}, username=None, password=None, ven_client_cert_key=None, ven_client_cert_pem=None, vtn_ca_certs=False, vtn_poll_interval=DEFAULT_VTN_POLL_INTERVAL, start_thread=True, client_id=None, ): ''' Sets up the class and intializes the HTTP client. event_config -- A dictionary containing key-word arugments for the EventHandller ven_client_cert_key -- Certification Key for the HTTP Client ven_client_cert_pem -- PEM file/string for the HTTP Client vtn_base_uri -- Base URI of the VTN's location vtn_poll_interval -- How often we should poll the VTN vtn_ca_certs -- CA Certs for the VTN start_thread -- start the thread for the poll loop or not? left as a legacy option ''' # Call the parent's methods super(OpenADR2, self).__init__(event_config, control_opts, client_id=client_id) # Get the VTN's base uri set self.vtn_base_uri = vtn_base_uri if self.vtn_base_uri: # append path join_char = '/' if self.vtn_base_uri[-1] != '/' else '' self.vtn_base_uri = join_char.join( (self.vtn_base_uri, OADR2_URI_PATH)) try: self.vtn_poll_interval = int(vtn_poll_interval) assert self.vtn_poll_interval >= MINIMUM_POLL_INTERVAL except ValueError: logger.warning('Invalid poll interval: %s', self.vtn_poll_interval) self.vtn_poll_interval = DEFAULT_VTN_POLL_INTERVAL # Security & Authentication related self.ven_certs = (ven_client_cert_pem, ven_client_cert_key)\ if ven_client_cert_pem and ven_client_cert_key else None self.vtn_ca_certs = vtn_ca_certs self.__username = username self.__password = password self.poll_thread = None if start_thread: # this is left for backward compatibility self.start() logger.info("+++++++++++++++ OADR2 module started ++++++++++++++")
def stop(self): ''' Stops polling without stopping event controller :return: ''' if self.poll_thread is not None: self.poll_thread.join(2) # they are daemons. self._exit.set() logger.info("Polling thread stopped")
def start(self): ''' Initialize the HTTP client. start_thread -- To start the polling thread or not. ''' if self.poll_thread and self.poll_thread.is_alive(): logger.warning("Thread is already running") return self.poll_thread = threading.Thread(name='oadr2.poll', target=self.poll_vtn_loop) self.poll_thread.daemon = True self._exit.clear() self.poll_thread.start() logger.info("Polling thread started")
def poll_vtn_loop(self): ''' The threading loop which polls the VTN on an interval ''' while not self._exit.is_set(): try: self.query_vtn() except urllib.error.HTTPError as ex: # 4xx or 5xx HTTP response: logger.warning("HTTP error: %s\n%s", ex, ex.read()) except urllib.error.URLError as ex: # network error. logger.debug("Network error: %s", ex) except Exception as ex: logger.exception("Error in OADR2 poll thread: %s", ex) self._exit.wait( uniform(self.vtn_poll_interval * (1 - POLLING_JITTER), self.vtn_poll_interval * (1 + POLLING_JITTER))) logger.info("+++++++++++++++ OADR2 polling thread has exited.")
def handle_payload(self, payload): ''' Handle a payload. Puts Events into the handler's event list. payload -- An lxml.etree.Element object of oadr:oadrDistributeEvent as root node Returns: An lxml.etree.Element object; which should be used as a response payload ''' reply_events = [] all_events = [] requestID = payload.findtext('pyld:requestID', namespaces=self.ns_map) vtnID = payload.findtext('ei:vtnID', namespaces=self.ns_map) # If we got a payload from an VTN that is not in our list, # send it a 400 message and return if self.vtn_ids and (vtnID not in self.vtn_ids): logger.warning("Unexpected VTN ID: %s, expected one of %r", vtnID, self.vtn_ids) return self.build_error_response(requestID, '400', 'Unknown vtnID: %s' % vtnID) # Loop through all of the oadr:oadrEvent 's in the payload for evt in payload.iterfind('oadr:oadrEvent', namespaces=self.ns_map): response_required = evt.findtext("oadr:oadrResponseRequired", namespaces=self.ns_map) evt = evt.find('ei:eiEvent', namespaces=self.ns_map) # go to nested eiEvent new_event = EventSchema.from_xml(evt) current_signal_val = get_current_signal_value(evt, self.ns_map) logger.debug( f'------ EVENT ID: {new_event.id}({new_event.mod_number}); ' f'Status: {new_event.status}; Current Signal: {current_signal_val}' ) all_events.append(new_event.id) old_event = self.db.get_event(new_event.id) # For the events we need to reply to, make our "opts," and check the status of the event # By default, we optIn and have an "OK," status (200) opt = 'optIn' status = '200' if old_event and (old_event.mod_number > new_event.mod_number): logger.warning( f"Got a smaller modification number " f"({new_event.mod_number} < {old_event.mod_number}) for event {new_event.id}" ) status = '403' opt = 'optOut' if not self.check_target_info(new_event): logger.info( f"Opting out of event {new_event.id} - no target match") status = '403' opt = 'optOut' if new_event.id in self.optouts: logger.info( f"Opting out of event {new_event.id} - user opted out") status = '200' opt = 'optOut' if not new_event.signals: logger.info( f"Opting out of event {new_event.id} - no simple signal") opt = 'optOut' status = '403' if self.market_contexts and (new_event.market_context not in self.market_contexts): logger.info( f"Opting out of event {new_event.id}:" f"market context {new_event.market_context} does not match" ) opt = 'optOut' status = '405' if response_required == 'always': reply_events.append((new_event.id, new_event.mod_number, requestID, opt, status)) # We have a new event or an updated old one # if (old_event is None) or (e_mod_num > old_mod_num): if opt == "optIn": if old_event and (old_event.mod_number < new_event.mod_number): # Add/update the event to our list # updated_events[e_id] = evt if new_event.status == "cancelled": if new_event.status != old_event.status: new_event.cancel(random_end=True) else: new_event.cancel() self.db.update_event(new_event) if not old_event: if new_event.status == "cancelled": new_event.cancel() self.db.add_event(new_event) # Find implicitly cancelled events and get rid of them for evt in self.get_active_events(): if evt.id not in all_events: logger.debug(f'Mark event {evt.id} as cancelled') evt.cancel() self.db.update_event(evt) # If we have any in the reply_events list, build some payloads logger.debug("Replying for events %r", reply_events) reply = None if reply_events: reply = self.build_created_payload(reply_events) return reply