Example #1
0
    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 ++++++++++++++")
Example #2
0
    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")
Example #3
0
    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.")
Example #4
0
    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
Example #5
0
    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
Example #6
0
    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