Skip to content

Commit

Permalink
Merge pull request #170 from ertis-research/main
Browse files Browse the repository at this point in the history
OpenADR 2.0b VEN client QualityLogic certification
  • Loading branch information
axmsoftware authored Aug 6, 2024
2 parents 815a85d + 523955a commit dd17127
Show file tree
Hide file tree
Showing 7 changed files with 544 additions and 128 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ dist/
.ipynb_checkpoints/
static/
.idea/
.vscode/
*.venv/
494 changes: 371 additions & 123 deletions openleadr/client.py

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions openleadr/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,70 @@ class ReportSpecifier:
class ReportRequest:
report_request_id: str
report_specifier: ReportSpecifier


@dataclass
class VavailabilityComponent:
dtstart: datetime
duration: timedelta


@dataclass
class Vavailability:
components: List[VavailabilityComponent]


@dataclass
class Opt:
opt_type: str
opt_reason: str
opt_id: str = None
created_date_time: datetime = None

event_id: str = None
modification_number: int = None
vavailability: Vavailability = None
targets: List[Target] = None
targets_by_type: Dict = None
market_context: str = None
signal_target_mrid: str = None

def __post_init__(self):
if self.opt_type not in enums.OPT.values:
raise ValueError(f"""The opt_type must be one of '{"', '".join(enums.OPT.values)}', """
f"""you specified: '{self.opt_type}'.""")
if self.opt_reason not in enums.OPT_REASON.values:
raise ValueError(f"""The opt_reason must be one of '{"', '".join(enums.OPT_REASON.values)}', """
f"""you specified: '{self.opt_type}'.""")
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-'):
raise ValueError(f"""The signal_target_mrid must be one of '{"', '".join(enums.SIGNAL_TARGET_MRID.values)}', """
f"""you specified: '{self.signal_target_mrid}'.""")
if self.event_id is None and self.vavailability is None:
raise ValueError(
"You must supply either 'event_id' or 'vavailability'.")
if self.event_id is not None and self.vavailability is not None:
raise ValueError(
"You supplied both 'event_id' and 'vavailability."
"Please supply either, but not both.")
if self.created_date_time is None:
self.created_date_time = datetime.now(timezone.utc)
if self.modification_number is None:
self.modification_number = 0
if self.targets is None and self.targets_by_type is None:
raise ValueError(
"You must supply either 'targets' or 'targets_by_type'.")
if 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}")
77 changes: 75 additions & 2 deletions openleadr/service/opt_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,82 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from . import service, VTNService
from . import service, handler, VTNService
import logging
logger = logging.getLogger('openleadr')

# ╔══════════════════════════════════════════════════════════════════════════╗
# ║ OPT SERVICE ║
# ╚══════════════════════════════════════════════════════════════════════════╝
# ┌──────────────────────────────────────────────────────────────────────────┐
# │ The VEN can send an Opt-in / Opt-out schedule to the VTN: │
# │ │
# │ ┌────┐ ┌────┐ │
# │ │VEN │ │VTN │ │
# │ └─┬──┘ └─┬──┘ │
# │ │───────────────────────────oadrCreateOpt()──────────────────────▶│ │
# │ │ │ │
# │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCreatedOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│ │
# │ │ │ │
# │ │
# └──────────────────────────────────────────────────────────────────────────┘
# ┌──────────────────────────────────────────────────────────────────────────┐
# │ The VEN can cancel a sent Opt-in / Opt-out schedule: │
# │ │
# │ ┌────┐ ┌────┐ │
# │ │VEN │ │VTN │ │
# │ └─┬──┘ └─┬──┘ │
# │ │───────────────────────────oadrCancelOpt()──────────────────────▶│ │
# │ │ │ │
# │ │◀ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ oadrCanceledOpt()─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
# │ │ │ │
# │ │
# └──────────────────────────────────────────────────────────────────────────┘


@service('EiOpt')
class OptService(VTNService):
pass

def __init__(self, vtn_id):
super().__init__(vtn_id)
self.created_opt_schedules = {}

@handler('oadrCreateOpt')
async def create_opt(self, payload):
"""
Handle an opt schedule created by the VEN
"""

pass # TODO: call handler and return the result (oadrCreatedOpt)

def on_create_opt(self, payload):
"""
Implementation of the on_create_opt handler, may be overwritten by the user.
"""
ven_id = payload['ven_id']

if payload['ven_id'] not in self.created_opt_schedules:
self.created_opt_schedules[ven_id] = []

# TODO: internally create an opt schedule and save it, if this is an optional handler then make sure to handle None returns

return 'oadrCreatedOpt', {'opt_id': payload['opt_id']}

@handler('oadrCancelOpt')
async def cancel_opt(self, payload):
"""
Cancel an opt schedule previously created by the VEN
"""
ven_id = payload['ven_id']
opt_id = payload['opt_id']

pass # TODO: call handler and return result (oadrCanceledOpt)

def on_cancel_opt(self, ven_id, opt_id):
"""
Placeholder for the on_cancel_opt handler.
"""

# TODO: implement cancellation of previously acknowledged opt schedule, if this is an optional handler make sure to hande None returns

return 'oadrCanceledOpt', {'opt_id': opt_id}
16 changes: 14 additions & 2 deletions openleadr/templates/oadrCreateOpt.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@
<emix:marketContext>{{ market_context }}</emix:marketContext>
{% endif %}
<ei:venID>{{ ven_id }}</ei:venID>
{% if vavailability is defined and vavalailability is not none %}
{% if vavailability is defined and vavailability is not none %}
<xcal:vavailability>
<xcal:components>
{% for component in vavailability.components %}
<xcal:available>
<xcal:properties>
<xcal:dtstart>
<xcal:date-time>{{ component.dtstart|datetimeformat }}</xcal:date-time></xcal:dtstart>
<xcal:date-time>{{ component.dtstart|datetimeformat }}</xcal:date-time>
</xcal:dtstart>
<xcal:duration>
<xcal:duration>{{ component.duration|timedeltaformat }}</xcal:duration>
</xcal:duration>
Expand All @@ -30,8 +31,19 @@
<ei:modificationNumber>{{ modification_number }}</ei:modificationNumber>
</ei:qualifiedEventID>
{% endif %}
{% if targets is defined and targets is not none and targets|length > 0 %}
{% for target in targets %}
{% include 'parts/eiTarget.xml' %}
{% endfor %}
{% else %}
<ei:eiTarget/>
{% endif %}
{% if signal_target_mrid is defined and signal_target_mrid is not none %}
<oadr:oadrDeviceClass>
<power:endDeviceAsset>
<power:mrid>{{ signal_target_mrid }}</power:mrid>
</power:endDeviceAsset>
</oadr:oadrDeviceClass>
{% endif %}
</oadr:oadrCreateOpt>
</oadr:oadrSignedObject>
10 changes: 10 additions & 0 deletions openleadr/templates/oadrCreatedOpt.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<oadr:oadrSignedObject xmlns:oadr="http://openadr.org/oadr-2.0b/2012/07" oadr:Id="oadrSignedObject">
<oadr:oadrCreatedOpt ei:schemaVersion="2.0b" xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110">
<ei:eiResponse>
<ei:responseCode>{{ response.response_code }}</ei:responseCode>
<ei:responseDescription>{{ response.response_description }}</ei:responseDescription>
<requestID xmlns="http://docs.oasis-open.org/ns/energyinterop/201110/payloads">{{ response.request_id }}</requestID>
</ei:eiResponse>
<ei:optID>{{ opt_id }}</ei:optID>
</oadr:oadrCreatedOpt>
</oadr:oadrSignedObject>
7 changes: 6 additions & 1 deletion openleadr/templates/oadrUpdateReport.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@
{% endif %}

{% if report.intervals %}
<strm:intervals xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream">
<strm:intervals xmlns:strm="urn:ietf:params:xml:ns:icalendar-2.0:stream" xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0">
{% for interval in report.intervals %}
<ei:interval>
<xcal:dtstart>
<xcal:date-time>{{ interval.dtstart|datetimeformat }}</xcal:date-time>
</xcal:dtstart>
{% if interval.duration is defined and interval.duration is not none %}
<xcal:duration>
<xcal:duration>{{ interval.duration|timedeltaformat }}</xcal:duration>
</xcal:duration>
{% endif %}
<oadr:oadrReportPayload>
<ei:rID>{{ interval.report_payload.r_id }}</ei:rID>
{% if interval.report_payload.confidence is defined and interval.report_payload.confidence is not none %}
Expand Down

0 comments on commit dd17127

Please sign in to comment.