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 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 query_vtn(self): ''' Query the VTN for an event. ''' if not self.vtn_base_uri: logger.warning("VTN base URI is invalid: %s", self.vtn_base_uri) return event_uri = self.vtn_base_uri + 'EiEvent' payload = self.event_handler.build_request_payload() logger.debug( f'New polling request to {event_uri}:\n' f'{etree.tostring(payload, pretty_print=True).decode("utf-8")}') try: resp = requests.post(event_uri, cert=self.ven_certs, verify=self.vtn_ca_certs, data=etree.tostring(payload), auth=(self.__username, self.__password) if self.__username or self.__password else None) except Exception as ex: logger.warning(f"Connection failed: {ex}") return reply = None try: payload = etree.fromstring(resp.content) logger.debug( f'Got Payload:\n' f'{etree.tostring(payload, pretty_print=True).decode("utf-8")}' ) reply = self.event_handler.handle_payload(payload) except Exception as ex: logger.warning(f"Connection failed: error parsing payload\n" f"{ex}: {resp.content}") # If we have a generated reply: if reply is not None: logger.debug( f'Reply to {event_uri}:\n' f'{etree.tostring(reply, pretty_print=True).decode("utf-8")}') # tell the control loop that events may have updated # (note `self.event_controller` is defined in base.BaseHandler) self.event_controller.events_updated() self.send_reply(reply, event_uri) # And send the response
def _calculate_current_event_status(self, events: List[EventSchema]): ''' returns a 3-tuple of (current_signal_level, current_event_id, remove_events=[]) ''' highest_signal_val = 0 current_event = None remove_events = [] # to collect expired events now = datetime.utcnow() for evt in events: try: if evt.status is None: logger.debug(f"Ignoring event {evt.id} - no valid status") continue if evt.status.lower( ) == "cancelled" and datetime.utcnow() > evt.end: logger.debug( f"Event {evt.id}({evt.mod_number}) has been cancelled") remove_events.append(evt.id) continue if not evt.signals: logger.debug(f"Ignoring event {evt.id} - no valid signals") continue current_interval = evt.get_current_interval(now=now) if current_interval is None: if evt.end < now: logger.debug( f"Event {evt.id}({evt.mod_number}) has ended") remove_events.append(evt.id) continue elif evt.start > now: logger.debug( f"Event {evt.id}({evt.mod_number}) has not started yet." ) continue else: logger.warning( f"Error getting current interval for event {evt.id}({evt.mod_number}):" f"Signals: {evt.signals}") continue if evt.test_event: logger.debug(f"Ignoring event {evt.id} - test event") continue logger.debug( f'Control loop: Evt ID: {evt.id}({evt.mod_number}); ' f'Interval: {current_interval.index}; Current Signal: {current_interval.level}' ) if current_interval.level > highest_signal_val or not current_event: if not current_event or evt.priority > current_event.priority: highest_signal_val = current_interval.level current_event = evt except Exception as ex: logger.exception(f"Error parsing event: {evt.id}: {ex}") return highest_signal_val, current_event.id if current_event else None, remove_events
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