예제 #1
0
    def build_created_payload(self, events):
        '''
        Assemble an XML payload to send out for events marked "response
        required."

        events -- List of tuples with the following structure:
                    (Event ID, Modification Number, Request ID,
                     Opt, Status)

        Returns: An XML Tree in a string
        '''

        # Setup the element makers
        oadr = ElementMaker(namespace=self.ns_map['oadr'], nsmap=self.ns_map)
        pyld = ElementMaker(namespace=self.ns_map['pyld'], nsmap=self.ns_map)
        ei = ElementMaker(namespace=self.ns_map['ei'], nsmap=self.ns_map)

        def responses(events):
            for e_id, mod_num, requestID, opt, status in events:
                yield ei.eventResponse(
                    ei.responseCode(str(status)), pyld.requestID(requestID),
                    ei.qualifiedEventID(ei.eventID(e_id),
                                        ei.modificationNumber(str(mod_num))),
                    ei.optType(opt))

        payload = oadr.oadrCreatedEvent(
            pyld.eiCreatedEvent(
                ei.eiResponse(ei.responseCode('200'), pyld.requestID()),
                ei.eventResponses(*list(responses(events))),
                ei.venID(self.ven_id)))

        logger.debug("Created payload:\n%s",
                     etree.tostring(payload, pretty_print=True))
        return payload
예제 #2
0
    def send_reply(self, payload, uri):
        '''
        Send a reply back to the VTN.

        payload -- An lxml.etree.ElementTree object containing an OpenADR 2.0
                   payload
        uri -- The URI (of the VTN) where the response should be sent
        '''

        resp = requests.post(uri,
                             cert=self.ven_certs,
                             verify=self.vtn_ca_certs,
                             data=etree.tostring(payload),
                             timeout=REQUEST_TIMEOUT,
                             auth=(self.__username, self.__password)
                             if self.__username or self.__password else None)

        logger.debug("EiEvent response: %s", resp.status_code)
예제 #3
0
    def _update_control(self, events):
        '''
        Called by `control_event_loop()` to determine the current signal level.
        This also deletes any events from the database that have expired.

        events -- List of lxml.etree.ElementTree objects (with OpenADR 2.0 tags)
        '''
        signal_level, event_id, remove_events = self._calculate_current_event_status(
            events)

        if remove_events:
            # remove any events that we've detected have ended or been cancelled.
            # TODO callback for expired events??
            logger.debug("Removing completed or cancelled events: %s",
                         remove_events)
            self.event_handler.remove_events(remove_events)

        if event_id:
            self.event_handler.update_active_status(event_id)

        return signal_level
예제 #4
0
    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.")
예제 #5
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.")
예제 #6
0
    def build_error_response(self, request_id, code, description=None):
        '''
        Assemble the XML for an error response payload.

        request_id -- Request ID of offending payload
        code -- The HTTP Error Code Status we want to use
        description -- An extra note on what was not acceptable

        Returns: An lxml.etree.Element object containing the payload
        '''

        oadr = ElementMaker(namespace=self.ns_map['oadr'], nsmap=self.ns_map)
        pyld = ElementMaker(namespace=self.ns_map['pyld'], nsmap=self.ns_map)
        ei = ElementMaker(namespace=self.ns_map['ei'], nsmap=self.ns_map)

        payload = oadr.oadrCreatedEvent(
            pyld.eiCreatedEvent(
                ei.eiResponse(ei.responseCode(code),
                              pyld.requestID(request_id)),
                ei.venID(self.ven_id)))

        logger.debug("Error payload:\n%s",
                     etree.tostring(payload, pretty_print=True))
        return payload
예제 #7
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
예제 #8
0
 def default_signal_callback(self, old_level, new_level):
     '''
     The default callback just logs a message.
     '''
     logger.debug(f"Signal level changed from {old_level} to {new_level}")
예제 #9
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
예제 #10
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