def __post_init__(self): if self.targets is None and self.targets_by_type is None: return elif self.targets_by_type is None: list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] self.targets_by_type = group_targets_by_type(list_of_targets) elif self.targets is None: self.targets = [Target(**target) for target in ungroup_targets_by_type(self.targets_by_type)] elif self.targets is not None and self.targets_by_type is not None: list_of_targets = [asdict(target) if is_dataclass(target) else target for target in self.targets] if group_targets_by_type(list_of_targets) != self.targets_by_type: raise ValueError("You assigned both 'targets' and 'targets_by_type' in your event, " "but the two were not consistent with each other. " f"You supplied 'targets' = {self.targets} and " f"'targets_by_type' = {self.targets_by_type}")
def __post_init__(self): if self.active_period is None: dtstart = min([ i['dtstart'] if isinstance(i, dict) else i.dtstart for s in self.event_signals for i in s.intervals ]) duration = max([ i['dtstart'] + i['duration'] if isinstance(i, dict) else i.dtstart + i.duration for s in self.event_signals for i in s.intervals ]) - dtstart self.active_period = ActivePeriod(dtstart=dtstart, duration=duration) if self.targets is None and self.targets_by_type is None: raise ValueError( "You must supply either 'targets' or 'targets_by_type'.") elif self.targets_by_type is None: list_of_targets = [ asdict(target) if is_dataclass(target) else target for target in self.targets ] self.targets_by_type = utils.group_targets_by_type(list_of_targets) elif self.targets is None: self.targets = [ Target(**target) for target in utils.ungroup_targets_by_type( self.targets_by_type) ] elif self.targets is not None and self.targets_by_type is not None: list_of_targets = [ asdict(target) if is_dataclass(target) else target for target in self.targets ] if utils.group_targets_by_type( list_of_targets) != self.targets_by_type: raise ValueError( "You assigned both 'targets' and 'targets_by_type' in your event, " "but the two were not consistent with each other. " f"You supplied 'targets' = {self.targets} and " f"'targets_by_type' = {self.targets_by_type}") # Set the event status self.event_descriptor.event_status = utils.determine_event_status( self.active_period)
def __post_init__(self): if self.signal_type not in enums.SIGNAL_TYPE.values: raise ValueError( f"""The signal_type must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ f"""you specified: '{self.signal_type}'.""") if self.signal_name not in enums.SIGNAL_NAME.values and not self.signal_name.startswith( 'x-'): raise ValueError( f"""The signal_name must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ f"""or it must begin with 'x-'. You specified: '{self.signal_name}'""" ) if self.targets is None and self.targets_by_type is None: return elif self.targets_by_type is None: list_of_targets = [ asdict(target) if is_dataclass(target) else target for target in self.targets ] targets_by_type = utils.group_targets_by_type(list_of_targets) if len(targets_by_type) > 1: raise ValueError( "In OpenADR, the EventSignal target may only be of type endDeviceAsset. " f"You provided types: '{', '.join(targets_by_type)}'") elif self.targets is None: self.targets = [ Target(**target) for target in utils.ungroup_targets_by_type( self.targets_by_type) ] elif self.targets is not None and self.targets_by_type is not None: list_of_targets = [ asdict(target) if is_dataclass(target) else target for target in self.targets ] if utils.group_targets_by_type( list_of_targets) != self.targets_by_type: raise ValueError( "You assigned both 'targets' and 'targets_by_type' in your event, " "but the two were not consistent with each other. " f"You supplied 'targets' = {self.targets} and " f"'targets_by_type' = {self.targets_by_type}")
def _preflight_oadrDistributeEvent(message_payload): if 'parse_duration' not in globals(): from .utils import parse_duration # Check that the total event_duration matches the sum of the interval durations (rule 8) for event in message_payload['events']: active_period_duration = event['active_period']['duration'] signal_durations = [] for signal in event['event_signals']: signal_durations.append( sum([ parse_duration(i['duration']) for i in signal['intervals'] ], timedelta(seconds=0))) if not all([d == active_period_duration for d in signal_durations]): if not all([d == signal_durations[0] for d in signal_durations]): raise ValueError( "The different EventSignals have different total durations. " "Please correct this.") else: logger.warning( f"The active_period duration for event " f"{event['event_descriptor']['event_id']} ({active_period_duration})" f" differs from the sum of the interval's durations " f"({signal_durations[0]}). The active_period duration has been " f"adjusted to ({signal_durations[0]}).") event['active_period']['duration'] = signal_durations[0] # Check that payload values with signal name SIMPLE are constricted (rule 9) for event in message_payload['events']: for event_signal in event['event_signals']: if event_signal['signal_name'] == "SIMPLE": for interval in event_signal['intervals']: if interval['signal_payload'] not in (0, 1, 2, 3): raise ValueError( "Payload Values used with Signal Name SIMPLE " "must be one of 0, 1, 2 or 3") # Check that the current_value is 0 for SIMPLE events that are not yet active (rule 14) for event in message_payload['events']: for event_signal in event['event_signals']: if 'current_value' in event_signal and event_signal[ 'current_value'] != 0: if event_signal['signal_name'] == "SIMPLE" \ and event['event_descriptor']['event_status'] != "ACTIVE": logger.warning("The current_value for a SIMPLE event " "that is not yet active must be 0. " "This will be corrected.") event_signal['current_value'] = 0 # Check that there is a valid oadrResponseRequired value for each Event for event in message_payload['events']: if 'response_required' not in event: event['response_required'] = 'always' elif event['response_required'] not in ('never', 'always'): logger.warning( f"The response_required property in an Event " f"should be 'never' or 'always', not " f"{event['response_required']}. Changing to 'always'.") event['response_required'] = 'always' # Check that there is a valid oadrResponseRequired value for each Event for event in message_payload['events']: if 'created_date_time' not in event['event_descriptor'] \ or not event['event_descriptor']['created_date_time']: event['event_descriptor']['created_date_time'] = datetime.now( timezone.utc) # Check that the target designations are correct and consistent for event in message_payload['events']: if 'targets' in event and 'targets_by_type' in event: if utils.group_targets_by_type( event['targets']) != event['targets_by_type']: raise ValueError( "You assigned both 'targets' and 'targets_by_type' in your event, " "but the two were not consistent with each other. " f"You supplied 'targets' = {event['targets']} and " f"'targets_by_type' = {event['targets_by_type']}") elif 'targets_by_type' in event and 'targets' not in event: event['targets'] = utils.ungroup_targets_by_type( event['targets_by_type'])
def test_ungroup_target_by_type_with_single_str(): targets_by_type = {'ven_id': 'ven123'} targets = utils.ungroup_targets_by_type(targets_by_type) assert targets == [{'ven_id': 'ven123'}]
def add_event(self, ven_id, signal_name, signal_type, intervals, callback, targets=None, targets_by_type=None, target=None, market_context="oadr://unknown.context"): """ Convenience method to add an event with a single signal. :param str ven_id: The ven_id to whom this event must be delivered. :param str signal_name: The OpenADR name of the signal; one of openleadr.objects.SIGNAL_NAME :param str signal_type: The OpenADR type of the signal; one of openleadr.objects.SIGNAL_TYPE :param str intervals: A list of intervals with a dtstart, duration and payload member. :param str callback: A callback function for when your event has been accepted (optIn) or refused (optOut). :param list targets: A list of Targets that this Event applies to. :param target: A single target for this event. :param dict targets_by_type: A dict of targets, grouped by type. :param str market_context: A URI for the DR program that this event belongs to. If you don't provide a target using any of the three arguments, the target will be set to the given ven_id. """ if self.services['event_service'].polling_method == 'external': logger.error( "You cannot use the add_event method after you assign your own on_poll " "handler. If you use your own on_poll handler, you are responsible for " "delivering events from that handler. If you want to use OpenLEADRs " "message queuing system, you should not assign an on_poll handler. " "Your Event will NOT be added.") return if not re.match( r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?", market_context): raise ValueError("The Market Context must be a valid URI.") event_id = utils.generate_id() # Figure out the target for this Event if target is None and targets is None and targets_by_type is None: targets = [{'ven_id': ven_id}] elif target is not None: targets = [target] elif targets_by_type is not None: targets = utils.ungroup_targets_by_type(targets_by_type) if not isinstance(targets, list): targets = [targets] event_descriptor = objects.EventDescriptor( event_id=event_id, modification_number=0, market_context=market_context, event_status="near", created_date_time=datetime.now(timezone.utc)) event_signal = objects.EventSignal(intervals=intervals, signal_name=signal_name, signal_type=signal_type, signal_id=utils.generate_id(), targets=targets) event = objects.Event(event_descriptor=event_descriptor, event_signals=[event_signal], targets=targets) if ven_id not in self.message_queues: self.message_queues[ven_id] = asyncio.Queue() self.message_queues[ven_id].put_nowait(event) self.services['event_service'].pending_events[event_id] = callback
def add_event(self, ven_id, signal_name, signal_type, intervals, callback=None, event_id=None, targets=None, targets_by_type=None, target=None, response_required='always', market_context="oadr://unknown.context", notification_period=None, ramp_up_period=None, recovery_period=None, signal_target_mrid=None): """ Convenience method to add an event with a single signal. :param str ven_id: The ven_id to whom this event must be delivered. :param str signal_name: The OpenADR name of the signal; one of openleadr.objects.SIGNAL_NAME :param str signal_type: The OpenADR type of the signal; one of openleadr.objects.SIGNAL_TYPE :param str intervals: A list of intervals with a dtstart, duration and payload member. :param str callback: A callback function for when your event has been accepted (optIn) or refused (optOut). :param list targets: A list of Targets that this Event applies to. :param target: A single target for this event. :param dict targets_by_type: A dict of targets, grouped by type. :param str market_context: A URI for the DR program that this event belongs to. :param timedelta notification_period: The Notification period for the Event's Active Period. :param timedelta ramp_up_period: The Ramp Up period for the Event's Active Period. :param timedelta recovery_period: The Recovery period for the Event's Active Period. If you don't provide a target using any of the three arguments, the target will be set to the given ven_id. """ if self.services['event_service'].polling_method == 'external': logger.error("You cannot use the add_event method after you assign your own on_poll " "handler. If you use your own on_poll handler, you are responsible for " "delivering events from that handler. If you want to use OpenLEADRs " "message queuing system, you should not assign an on_poll handler. " "Your Event will NOT be added.") return if not re.match(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?", market_context): raise ValueError("The Market Context must be a valid URI.") event_id = event_id or utils.generate_id() if response_required not in ('always', 'never'): raise ValueError("'response_required' should be either 'always' or 'never'; " f"you provided '{response_required}'.") # Figure out the target for this Event if target is None and targets is None and targets_by_type is None: targets = [{'ven_id': ven_id}] elif target is not None: targets = [target] elif targets_by_type is not None: targets = utils.ungroup_targets_by_type(targets_by_type) if not isinstance(targets, list): targets = [targets] if signal_type not in enums.SIGNAL_TYPE.values: raise ValueError(f"""The signal_type must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ f"""you specified: '{signal_type}'.""") if signal_name not in enums.SIGNAL_NAME.values and not signal_name.startswith('x-'): raise ValueError(f"""The signal_name must be one of '{"', '".join(enums.SIGNAL_TYPE.values)}', """ f"""or it must begin with 'x-'. You specified: '{signal_name}'""") if not intervals or not isinstance(intervals, (list, tuple)) or len(intervals) == 0: raise ValueError(f"The intervals must be a list of intervals, you specified: {intervals}") event_descriptor = objects.EventDescriptor(event_id=event_id, modification_number=0, market_context=market_context, event_status="far", created_date_time=datetime.now(timezone.utc)) event_signal = objects.EventSignal(intervals=intervals, signal_name=signal_name, signal_type=signal_type, signal_id=utils.generate_id()) # Make sure the intervals carry timezone-aware timestamps for interval in intervals: if utils.getmember(interval, 'dtstart').tzinfo is None: utils.setmember(interval, 'dtstart', utils.getmember(interval, 'dtstart').astimezone(timezone.utc)) logger.warning("You supplied a naive datetime object to your interval's dtstart. " "This will be interpreted as a timestamp in your local timezone " "and then converted to UTC before sending. Please supply timezone-" "aware timestamps like datetime.datetime.new(timezone.utc) or " "datetime.datetime(..., tzinfo=datetime.timezone.utc)") active_period = utils.get_active_period_from_intervals(intervals, False) active_period.ramp_up_period = ramp_up_period active_period.notification_period = notification_period active_period.recovery_period = recovery_period event = objects.Event(active_period=active_period, event_descriptor=event_descriptor, event_signals=[event_signal], targets=targets, response_required=response_required) self.add_raw_event(ven_id=ven_id, event=event, callback=callback) return event_id