1919import logging
2020import ssl
2121from datetime import datetime , timedelta , timezone
22+ from dataclasses import asdict
2223from functools import partial
2324from http import HTTPStatus
2425
@@ -42,9 +43,11 @@ class OpenADRClient:
4243 Main client class. Most of these methods will be called automatically, but
4344 you can always choose to call them manually.
4445 """
46+
4547 def __init__ (self , ven_name , vtn_url , debug = False , cert = None , key = None ,
4648 passphrase = None , vtn_fingerprint = None , show_fingerprint = True , ca_file = None ,
47- allow_jitter = True , ven_id = None , disable_signature = False , check_hostname = True ):
49+ allow_jitter = True , ven_id = None , disable_signature = False , check_hostname = True ,
50+ event_status_log_period = 10 , events_clean_up_period = 300 ):
4851 """
4952 Initializes a new OpenADR Client (Virtual End Node)
5053
@@ -65,6 +68,9 @@ def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None,
6568 :param str ven_id: The ID for this VEN. If you leave this blank,
6669 a VEN_ID will be assigned by the VTN.
6770 :param bool disable_signature: Whether or not to sign outgoing messages using a public-private key pair in PEM format.
71+ :param bool check_hostname: Whether or not to check hostname
72+ :param int event_status_log_period: Setting the priod of status change logging
73+ :param int events_clean_up_period: Setting the priod of not relevant events clean up
6874 """
6975
7076 self .ven_name = ven_name
@@ -75,6 +81,8 @@ def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None,
7581 self .vtn_fingerprint = vtn_fingerprint
7682 self .debug = debug
7783 self .check_hostname = check_hostname
84+ self .event_status_log_period = event_status_log_period
85+ self .events_clean_up_period = events_clean_up_period
7886
7987 self .reports = []
8088 self .report_callbacks = {} # Holds the callbacks for each specific report
@@ -85,6 +93,7 @@ def __init__(self, ven_name, vtn_url, debug=False, cert=None, key=None,
8593 self .client_session = None
8694 self .report_queue_task = None
8795
96+ self .opts = []
8897 self .received_events = [] # Holds the events that we received.
8998 self .responded_events = {} # Holds the events that we already saw.
9099
@@ -162,9 +171,12 @@ async def run(self):
162171 self .scheduler .add_job (self ._poll ,
163172 trigger = 'interval' ,
164173 seconds = self .poll_frequency .total_seconds ())
174+ self .scheduler .add_job (self ._event_status_log ,
175+ trigger = 'interval' ,
176+ seconds = self .event_status_log_period )
165177 self .scheduler .add_job (self ._event_cleanup ,
166178 trigger = 'interval' ,
167- seconds = 300 )
179+ seconds = self . events_clean_up_period )
168180 self .scheduler .start ()
169181
170182 async def stop (self ):
@@ -558,6 +570,104 @@ async def sync_events(self):
558570 if 'events' in response_payload and len (response_payload ['events' ]) > 0 :
559571 await self ._on_event (response_payload )
560572
573+ ###########################################################################
574+ # #
575+ # OPT METHODS #
576+ # #
577+ ###########################################################################
578+
579+ async def create_opt (self , opt_type , opt_reason , targets , vavailability = None , event_id = None ,
580+ modification_number = None , opt_id = None , request_id = None , market_context = None ,
581+ signal_target_mrid = None ):
582+ """
583+ Send a new opt to the VTN, either to communicate a temporary availability
584+ schedule or to qualify the resources participating in an event.
585+
586+ :param str opt_type: An OpenADR opt type. (found in openleadr.enums.OPT)
587+ :param str opt_reason: An OpenADR opt reason. (found in openleadr.enums.OPT_REASON)
588+ :param targets: A list of target(s) that this opt is related to.
589+ :param vavailability: The availability schedule to send
590+ :param event_id: The id of the event this opt is referencing.
591+ :param modification_number: The modification number of the event this opt is referencing.
592+ :param str opt_id: A unique identifier for this opt message. Leave this blank for a
593+ random generated id, or fill it in if your VTN depends on
594+ this being a known value, or if it needs to be constant
595+ between restarts of the client.
596+ :param str request_id: A unique identifier for this request. The same remarks apply
597+ as for the opt_id.
598+ :param str market_context: The Market Context that this opt belongs to.
599+ """
600+
601+ # Verify input
602+ if opt_type not in enums .OPT .values :
603+ raise ValueError (f"{ opt_type } is not a valid opt type. Valid options are "
604+ f"{ ', ' .join (enums .REPORT_NAME .values )} " )
605+ if opt_reason not in enums .OPT_REASON .values :
606+ raise ValueError (f"{ opt_reason } is not a valid opt reason. Valid options are "
607+ f"{ ', ' .join (enums .REPORT_NAME .values )} " )
608+
609+ # Save opt
610+ opt_id = opt_id or utils .generate_id ()
611+ opt = objects .Opt (
612+ opt_id = opt_id ,
613+ opt_type = opt_type ,
614+ opt_reason = opt_reason ,
615+ vavailability = vavailability ,
616+ event_id = event_id ,
617+ modification_number = modification_number ,
618+ targets = targets ,
619+ market_context = market_context ,
620+ signal_target_mrid = signal_target_mrid
621+ )
622+ self .opts .append (opt )
623+
624+ # Send opt
625+ request_id = request_id or utils .generate_id ()
626+ payload = {
627+ 'request_id' : request_id ,
628+ 'ven_id' : self .ven_id ,
629+ ** asdict (opt )
630+ }
631+
632+ service = 'EiOpt'
633+ message = self ._create_message ('oadrCreateOpt' , ** payload )
634+ response_type , response_payload = await self ._perform_request (service , message )
635+
636+ if 'opt_id' in response_payload :
637+ # VTN acknowledged the opt message
638+ return response_payload ['opt_id' ]
639+
640+ # TODO: what to do if the VTN sends an error or does not acknowledge the opt?
641+
642+ async def cancel_opt (self , opt_id ):
643+ """
644+ Tell the VTN to cancel a previously acknowledged opt message
645+
646+ :param str opt_id: The id of the opt to cancel
647+ """
648+
649+ # Check if this opt exists
650+ opt = utils .find_by (
651+ self .opts , 'opt_id' , opt_id )
652+ if not opt :
653+ logger .error (f"A non-existant opt with opt_id "
654+ f"{ opt_id } was requested for cancellation." )
655+ return False
656+
657+ payload = {
658+ 'opt_id' : opt_id ,
659+ 'ven_id' : self .ven_id
660+ }
661+
662+ service = 'EiOpt'
663+ message = self ._create_message ('oadrCancelOpt' , ** payload )
664+ response_type , response_payload = await self ._perform_request (service , message )
665+
666+ if 'opt_id' in response_payload :
667+ # VTN acknowledged the opt cancelation
668+ self .opts .remove (opt )
669+ return True
670+
561671 ###########################################################################
562672 # #
563673 # REPORTING METHODS #
@@ -1020,7 +1130,6 @@ async def _execute_hooks(self, hook_name, *args, **kwargs):
10201130 f"{ err .__class__ .__name__ } : { err } " )
10211131
10221132 async def _on_event (self , message ):
1023- logger .debug ("The VEN received an event" )
10241133 events = message ['events' ]
10251134 invalid_vtn_id = False
10261135 try :
@@ -1033,6 +1142,7 @@ async def _on_event(self, message):
10331142 event_id = event ['event_descriptor' ]['event_id' ]
10341143 event_status = event ['event_descriptor' ]['event_status' ]
10351144 modification_number = event ['event_descriptor' ]['modification_number' ]
1145+ logger .info ("The VEN received an event with event_id: %s, status: %s, modification_number: %s" , event_id , event_status , modification_number ) # change to debug
10361146 received_event = utils .find_by (self .received_events , 'event_descriptor.event_id' , event_id )
10371147 if received_event :
10381148 if received_event ['event_descriptor' ]['modification_number' ] == modification_number :
@@ -1112,6 +1222,20 @@ async def _on_event(self, message):
11121222 else :
11131223 logger .info ("Not sending any event responses, because a response was not required/allowed by the VTN." )
11141224
1225+ async def _event_status_log (self ):
1226+ """
1227+ Periodic task that will log each event status change
1228+ """
1229+ for event in self .received_events :
1230+ # ignoring the cancelled case
1231+ if event ['event_descriptor' ]['event_status' ] == 'cancelled' :
1232+ continue
1233+
1234+ event_status = utils .determine_event_status (event ['active_period' ])
1235+ if event_status != event ['event_descriptor' ]['event_status' ]:
1236+ event ['event_descriptor' ]['event_status' ] = event_status
1237+ logger .info ("event_id: %s has new status: %s" , event ['event_descriptor' ]['event_id' ], event_status ) # change to debug
1238+
11151239 async def _event_cleanup (self ):
11161240 """
11171241 Periodic task that will clean up completed and cancelled events in our memory.
0 commit comments