Skip to content

Commit dd17127

Browse files
authored
Merge pull request #170 from ertis-research/main
OpenADR 2.0b VEN client QualityLogic certification
2 parents 815a85d + 523955a commit dd17127

File tree

7 files changed

+544
-128
lines changed

7 files changed

+544
-128
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ dist/
1111
.ipynb_checkpoints/
1212
static/
1313
.idea/
14+
.vscode/
1415
*.venv/

openleadr/client.py

Lines changed: 371 additions & 123 deletions
Large diffs are not rendered by default.

openleadr/objects.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,70 @@ class ReportSpecifier:
309309
class ReportRequest:
310310
report_request_id: str
311311
report_specifier: ReportSpecifier
312+
313+
314+
@dataclass
315+
class VavailabilityComponent:
316+
dtstart: datetime
317+
duration: timedelta
318+
319+
320+
@dataclass
321+
class Vavailability:
322+
components: List[VavailabilityComponent]
323+
324+
325+
@dataclass
326+
class Opt:
327+
opt_type: str
328+
opt_reason: str
329+
opt_id: str = None
330+
created_date_time: datetime = None
331+
332+
event_id: str = None
333+
modification_number: int = None
334+
vavailability: Vavailability = None
335+
targets: List[Target] = None
336+
targets_by_type: Dict = None
337+
market_context: str = None
338+
signal_target_mrid: str = None
339+
340+
def __post_init__(self):
341+
if self.opt_type not in enums.OPT.values:
342+
raise ValueError(f"""The opt_type must be one of '{"', '".join(enums.OPT.values)}', """
343+
f"""you specified: '{self.opt_type}'.""")
344+
if self.opt_reason not in enums.OPT_REASON.values:
345+
raise ValueError(f"""The opt_reason must be one of '{"', '".join(enums.OPT_REASON.values)}', """
346+
f"""you specified: '{self.opt_type}'.""")
347+
if self.signal_target_mrid is not None and self.signal_target_mrid not in enums.SIGNAL_TARGET_MRID.values and not self.signal_target_mrid.startswith('x-'):
348+
raise ValueError(f"""The signal_target_mrid must be one of '{"', '".join(enums.SIGNAL_TARGET_MRID.values)}', """
349+
f"""you specified: '{self.signal_target_mrid}'.""")
350+
if self.event_id is None and self.vavailability is None:
351+
raise ValueError(
352+
"You must supply either 'event_id' or 'vavailability'.")
353+
if self.event_id is not None and self.vavailability is not None:
354+
raise ValueError(
355+
"You supplied both 'event_id' and 'vavailability."
356+
"Please supply either, but not both.")
357+
if self.created_date_time is None:
358+
self.created_date_time = datetime.now(timezone.utc)
359+
if self.modification_number is None:
360+
self.modification_number = 0
361+
if self.targets is None and self.targets_by_type is None:
362+
raise ValueError(
363+
"You must supply either 'targets' or 'targets_by_type'.")
364+
if self.targets_by_type is None:
365+
list_of_targets = [asdict(target) if is_dataclass(
366+
target) else target for target in self.targets]
367+
self.targets_by_type = utils.group_targets_by_type(list_of_targets)
368+
elif self.targets is None:
369+
self.targets = [Target(
370+
**target) for target in utils.ungroup_targets_by_type(self.targets_by_type)]
371+
elif self.targets is not None and self.targets_by_type is not None:
372+
list_of_targets = [asdict(target) if is_dataclass(
373+
target) else target for target in self.targets]
374+
if utils.group_targets_by_type(list_of_targets) != self.targets_by_type:
375+
raise ValueError("You assigned both 'targets' and 'targets_by_type' in your event, "
376+
"but the two were not consistent with each other. "
377+
f"You supplied 'targets' = {self.targets} and "
378+
f"'targets_by_type' = {self.targets_by_type}")

openleadr/service/opt_service.py

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,82 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
from . import service, VTNService
17+
from . import service, handler, VTNService
18+
import logging
19+
logger = logging.getLogger('openleadr')
20+
21+
# ╔══════════════════════════════════════════════════════════════════════════╗
22+
# ║ OPT SERVICE ║
23+
# ╚══════════════════════════════════════════════════════════════════════════╝
24+
# ┌──────────────────────────────────────────────────────────────────────────┐
25+
# │ The VEN can send an Opt-in / Opt-out schedule to the VTN: │
26+
# │ │
27+
# │ ┌────┐ ┌────┐ │
28+
# │ │VEN │ │VTN │ │
29+
# │ └─┬──┘ └─┬──┘ │
30+
# │ │───────────────────────────oadrCreateOpt()──────────────────────▶│ │
31+
# │ │ │ │
32+
# │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCreatedOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
33+
# │ │ │ │
34+
# │ │
35+
# └──────────────────────────────────────────────────────────────────────────┘
36+
# ┌──────────────────────────────────────────────────────────────────────────┐
37+
# │ The VEN can cancel a sent Opt-in / Opt-out schedule: │
38+
# │ │
39+
# │ ┌────┐ ┌────┐ │
40+
# │ │VEN │ │VTN │ │
41+
# │ └─┬──┘ └─┬──┘ │
42+
# │ │───────────────────────────oadrCancelOpt()──────────────────────▶│ │
43+
# │ │ │ │
44+
# │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCanceledOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
45+
# │ │ │ │
46+
# │ │
47+
# └──────────────────────────────────────────────────────────────────────────┘
1848

1949

2050
@service('EiOpt')
2151
class OptService(VTNService):
22-
pass
52+
53+
def __init__(self, vtn_id):
54+
super().__init__(vtn_id)
55+
self.created_opt_schedules = {}
56+
57+
@handler('oadrCreateOpt')
58+
async def create_opt(self, payload):
59+
"""
60+
Handle an opt schedule created by the VEN
61+
"""
62+
63+
pass # TODO: call handler and return the result (oadrCreatedOpt)
64+
65+
def on_create_opt(self, payload):
66+
"""
67+
Implementation of the on_create_opt handler, may be overwritten by the user.
68+
"""
69+
ven_id = payload['ven_id']
70+
71+
if payload['ven_id'] not in self.created_opt_schedules:
72+
self.created_opt_schedules[ven_id] = []
73+
74+
# TODO: internally create an opt schedule and save it, if this is an optional handler then make sure to handle None returns
75+
76+
return 'oadrCreatedOpt', {'opt_id': payload['opt_id']}
77+
78+
@handler('oadrCancelOpt')
79+
async def cancel_opt(self, payload):
80+
"""
81+
Cancel an opt schedule previously created by the VEN
82+
"""
83+
ven_id = payload['ven_id']
84+
opt_id = payload['opt_id']
85+
86+
pass # TODO: call handler and return result (oadrCanceledOpt)
87+
88+
def on_cancel_opt(self, ven_id, opt_id):
89+
"""
90+
Placeholder for the on_cancel_opt handler.
91+
"""
92+
93+
# TODO: implement cancellation of previously acknowledged opt schedule, if this is an optional handler make sure to hande None returns
94+
95+
return 'oadrCanceledOpt', {'opt_id': opt_id}

openleadr/templates/oadrCreateOpt.xml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
<emix:marketContext>{{ market_context }}</emix:marketContext>
88
{% endif %}
99
<ei:venID>{{ ven_id }}</ei:venID>
10-
{% if vavailability is defined and vavalailability is not none %}
10+
{% if vavailability is defined and vavailability is not none %}
1111
<xcal:vavailability>
1212
<xcal:components>
1313
{% for component in vavailability.components %}
1414
<xcal:available>
1515
<xcal:properties>
1616
<xcal:dtstart>
17-
<xcal:date-time>{{ component.dtstart|datetimeformat }}</xcal:date-time></xcal:dtstart>
17+
<xcal:date-time>{{ component.dtstart|datetimeformat }}</xcal:date-time>
18+
</xcal:dtstart>
1819
<xcal:duration>
1920
<xcal:duration>{{ component.duration|timedeltaformat }}</xcal:duration>
2021
</xcal:duration>
@@ -30,8 +31,19 @@
3031
<ei:modificationNumber>{{ modification_number }}</ei:modificationNumber>
3132
</ei:qualifiedEventID>
3233
{% endif %}
34+
{% if targets is defined and targets is not none and targets|length > 0 %}
3335
{% for target in targets %}
3436
{% include 'parts/eiTarget.xml' %}
3537
{% endfor %}
38+
{% else %}
39+
<ei:eiTarget/>
40+
{% endif %}
41+
{% if signal_target_mrid is defined and signal_target_mrid is not none %}
42+
<oadr:oadrDeviceClass>
43+
<power:endDeviceAsset>
44+
<power:mrid>{{ signal_target_mrid }}</power:mrid>
45+
</power:endDeviceAsset>
46+
</oadr:oadrDeviceClass>
47+
{% endif %}
3648
</oadr:oadrCreateOpt>
3749
</oadr:oadrSignedObject>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<oadr:oadrSignedObject xmlns:oadr="http://openadr.org/oadr-2.0b/2012/07" oadr:Id="oadrSignedObject">
2+
<oadr:oadrCreatedOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
3+
<ei:eiResponse>
4+
<ei:responseCode>{{ response.response_code }}</ei:responseCode>
5+
<ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
6+
<requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
7+
</ei:eiResponse>
8+
<ei:optID>{{ opt_id }}</ei:optID>
9+
</oadr:oadrCreatedOpt>
10+
</oadr:oadrSignedObject>

openleadr/templates/oadrUpdateReport.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@
1111
{% endif %}
1212

1313
{% if report.intervals %}
14-
<strm:intervals xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream">
14+
<strm:intervals xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream" xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0">
1515
{% for interval in report.intervals %}
1616
<ei:interval>
1717
<xcal:dtstart>
1818
<xcal:date-time>{{ interval.dtstart|datetimeformat }}</xcal:date-time>
1919
</xcal:dtstart>
20+
{% if interval.duration is defined and interval.duration is not none %}
21+
<xcal:duration>
22+
<xcal:duration>{{ interval.duration|timedeltaformat }}</xcal:duration>
23+
</xcal:duration>
24+
{% endif %}
2025
<oadr:oadrReportPayload>
2126
<ei:rID>{{ interval.report_payload.r_id }}</ei:rID>
2227
{% if interval.report_payload.confidence is defined and interval.report_payload.confidence is not none %}

0 commit comments

Comments
 (0)