openadr3-client-gac-compliance 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/PKG-INFO +1 -1
- openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/event_gac_compliant.py +416 -0
- openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/program_gac_compliant.py +101 -0
- openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +55 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/pyproject.toml +1 -1
- openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/event_gac_compliant.py +0 -200
- openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/program_gac_compliant.py +0 -39
- openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py +0 -27
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/LICENSE.md +0 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/README.md +0 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/openadr3_client_gac_compliance/__init__.py +0 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/openadr3_client_gac_compliance/config.py +0 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/openadr3_client_gac_compliance/gac20/__init__.py +0 -0
- {openadr3_client_gac_compliance-1.1.0 → openadr3_client_gac_compliance-1.2.0}/openadr3_client_gac_compliance/gac21/__init__.py +0 -0
openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/event_gac_compliant.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""Module which implements GAC compliance validators for the event OpenADR3 types.
|
|
2
|
+
|
|
3
|
+
This module validates all the object constraints and requirements on the OpenADR3 events resource
|
|
4
|
+
as specified in the Grid aware charging (GAC) specification.
|
|
5
|
+
|
|
6
|
+
There is one requirement that is not validated here, as it cannot be validated through the scope of the
|
|
7
|
+
pydantic validators. Namely, the requirement that a safe mode event MUST be present in a program.
|
|
8
|
+
|
|
9
|
+
As the pydantic validator works on the scope of a single Event Object, it is not possible to validate
|
|
10
|
+
that a safe mode event is present in a program. And it cannot be validated on the Program object,
|
|
11
|
+
as the program object does not contain the events, these are stored seperately in the VTN.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from itertools import pairwise
|
|
15
|
+
import re
|
|
16
|
+
from typing import Tuple
|
|
17
|
+
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
18
|
+
from openadr3_client.models.event.event import Event
|
|
19
|
+
from openadr3_client.models.event.event_payload import EventPayloadType
|
|
20
|
+
|
|
21
|
+
from pydantic import ValidationError
|
|
22
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _continuous_or_seperated(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
26
|
+
"""Enforces that events either have consistent interval definitions compliant with GAC.
|
|
27
|
+
|
|
28
|
+
the Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
|
|
29
|
+
interval definitions:
|
|
30
|
+
|
|
31
|
+
1. Continuous
|
|
32
|
+
2. Seperated
|
|
33
|
+
|
|
34
|
+
The continious implementation can be used when all intervals have the same duration.
|
|
35
|
+
In this case, only the top-level intervalPeriod of the event can be used, and the intervalPeriods
|
|
36
|
+
of the individual intervals must be None.
|
|
37
|
+
|
|
38
|
+
In the seperated intervalDefinition approach, the intervalPeriods must be set on each individual intervals,
|
|
39
|
+
and the top-level intervalPeriod of the event must be None. This seperated approach is used when events have differing
|
|
40
|
+
durations.
|
|
41
|
+
"""
|
|
42
|
+
validation_errors: list[InitErrorDetails] = []
|
|
43
|
+
|
|
44
|
+
intervals = self.intervals or ()
|
|
45
|
+
|
|
46
|
+
if self.interval_period is None:
|
|
47
|
+
# interval period not set at top level of the event.
|
|
48
|
+
# Ensure that all intervals have the interval_period defined, to comply with the GAC specification.
|
|
49
|
+
undefined_intervals_period = [i for i in intervals if i.interval_period is None]
|
|
50
|
+
if undefined_intervals_period:
|
|
51
|
+
validation_errors.append(
|
|
52
|
+
InitErrorDetails(
|
|
53
|
+
type=PydanticCustomError(
|
|
54
|
+
"value_error",
|
|
55
|
+
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
|
|
56
|
+
),
|
|
57
|
+
loc=("intervals",),
|
|
58
|
+
input=self.intervals,
|
|
59
|
+
ctx={},
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
# interval period set at top level of the event.
|
|
64
|
+
# Ensure that all intervals do not have the interval_period defined, to comply with the GAC specification.
|
|
65
|
+
duplicate_interval_period = [
|
|
66
|
+
i for i in intervals if i.interval_period is not None
|
|
67
|
+
]
|
|
68
|
+
if duplicate_interval_period:
|
|
69
|
+
validation_errors.append(
|
|
70
|
+
InitErrorDetails(
|
|
71
|
+
type=PydanticCustomError(
|
|
72
|
+
"value_error",
|
|
73
|
+
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'.",
|
|
74
|
+
),
|
|
75
|
+
loc=("intervals",),
|
|
76
|
+
input=self.intervals,
|
|
77
|
+
ctx={},
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return self, validation_errors
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _targets_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
85
|
+
"""Enforces that the targets of the event are compliant with GAC.
|
|
86
|
+
|
|
87
|
+
GAC enforces the following constraints for targets:
|
|
88
|
+
|
|
89
|
+
- The event must contain a POWER_SERVICE_LOCATIONS target.
|
|
90
|
+
- The POWER_SERVICE_LOCATIONS target value must be a list of 'EAN18' values.
|
|
91
|
+
- The event must contain a VEN_NAME target.
|
|
92
|
+
- The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).
|
|
93
|
+
"""
|
|
94
|
+
validation_errors: list[InitErrorDetails] = []
|
|
95
|
+
targets = self.targets or ()
|
|
96
|
+
|
|
97
|
+
power_service_locations = [
|
|
98
|
+
t for t in targets if t.type == "POWER_SERVICE_LOCATIONS"
|
|
99
|
+
]
|
|
100
|
+
ven_names = [t for t in targets if t.type == "VEN_NAME"]
|
|
101
|
+
|
|
102
|
+
if not power_service_locations:
|
|
103
|
+
validation_errors.append(
|
|
104
|
+
InitErrorDetails(
|
|
105
|
+
type=PydanticCustomError(
|
|
106
|
+
"value_error",
|
|
107
|
+
"The event must contain a POWER_SERVICE_LOCATIONS target.",
|
|
108
|
+
),
|
|
109
|
+
loc=("targets",),
|
|
110
|
+
input=self.targets,
|
|
111
|
+
ctx={},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if not ven_names:
|
|
116
|
+
validation_errors.append(
|
|
117
|
+
InitErrorDetails(
|
|
118
|
+
type=PydanticCustomError(
|
|
119
|
+
"value_error",
|
|
120
|
+
"The event must contain a VEN_NAME target.",
|
|
121
|
+
),
|
|
122
|
+
loc=("targets",),
|
|
123
|
+
input=self.targets,
|
|
124
|
+
ctx={},
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if len(power_service_locations) > 1:
|
|
129
|
+
validation_errors.append(
|
|
130
|
+
InitErrorDetails(
|
|
131
|
+
type=PydanticCustomError(
|
|
132
|
+
"value_error",
|
|
133
|
+
"The event must contain exactly one POWER_SERVICE_LOCATIONS target.",
|
|
134
|
+
),
|
|
135
|
+
loc=("targets",),
|
|
136
|
+
input=self.targets,
|
|
137
|
+
ctx={},
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if len(ven_names) > 1:
|
|
142
|
+
validation_errors.append(
|
|
143
|
+
InitErrorDetails(
|
|
144
|
+
type=PydanticCustomError(
|
|
145
|
+
"value_error",
|
|
146
|
+
"The event must contain only one VEN_NAME target.",
|
|
147
|
+
),
|
|
148
|
+
loc=("targets",),
|
|
149
|
+
input=self.targets,
|
|
150
|
+
ctx={},
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
power_service_locations
|
|
156
|
+
and ven_names
|
|
157
|
+
and len(power_service_locations) == 1
|
|
158
|
+
and len(ven_names) == 1
|
|
159
|
+
):
|
|
160
|
+
power_service_location = power_service_locations[0]
|
|
161
|
+
ven_name = ven_names[0]
|
|
162
|
+
|
|
163
|
+
if len(power_service_location.values) == 0:
|
|
164
|
+
validation_errors.append(
|
|
165
|
+
InitErrorDetails(
|
|
166
|
+
type=PydanticCustomError(
|
|
167
|
+
"value_error",
|
|
168
|
+
"The POWER_SERVICE_LOCATIONS target value cannot be empty.",
|
|
169
|
+
),
|
|
170
|
+
loc=("targets",),
|
|
171
|
+
input=self.targets,
|
|
172
|
+
ctx={},
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
if not all(re.fullmatch(r"\d{18}", v) for v in power_service_location.values):
|
|
177
|
+
validation_errors.append(
|
|
178
|
+
InitErrorDetails(
|
|
179
|
+
type=PydanticCustomError(
|
|
180
|
+
"value_error",
|
|
181
|
+
"The POWER_SERVICE_LOCATIONS target value must be a list of 'EAN18' values.",
|
|
182
|
+
),
|
|
183
|
+
loc=("targets",),
|
|
184
|
+
input=self.targets,
|
|
185
|
+
ctx={},
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if len(ven_name.values) == 0:
|
|
190
|
+
validation_errors.append(
|
|
191
|
+
InitErrorDetails(
|
|
192
|
+
type=PydanticCustomError(
|
|
193
|
+
"value_error",
|
|
194
|
+
"The VEN_NAME target value cannot be empty.",
|
|
195
|
+
),
|
|
196
|
+
loc=("targets",),
|
|
197
|
+
input=self.targets,
|
|
198
|
+
ctx={},
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if not all(1 <= len(v) <= 128 for v in ven_name.values):
|
|
203
|
+
validation_errors.append(
|
|
204
|
+
InitErrorDetails(
|
|
205
|
+
type=PydanticCustomError(
|
|
206
|
+
"value_error",
|
|
207
|
+
"The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).",
|
|
208
|
+
),
|
|
209
|
+
loc=("targets",),
|
|
210
|
+
input=self.targets,
|
|
211
|
+
ctx={},
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return self, validation_errors
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _payload_descriptor_gac_compliant(
|
|
219
|
+
self: Event,
|
|
220
|
+
) -> Tuple[Event, list[InitErrorDetails]]:
|
|
221
|
+
"""Enforces that the payload descriptor is GAC compliant.
|
|
222
|
+
|
|
223
|
+
GAC enforces the following constraints for payload descriptors:
|
|
224
|
+
|
|
225
|
+
- The event interval must exactly one payload descriptor.
|
|
226
|
+
- The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'
|
|
227
|
+
- The payload descriptor must have a units of 'KW' (case sensitive).
|
|
228
|
+
"""
|
|
229
|
+
validation_errors: list[InitErrorDetails] = []
|
|
230
|
+
|
|
231
|
+
if self.payload_descriptor is None:
|
|
232
|
+
validation_errors.append(
|
|
233
|
+
InitErrorDetails(
|
|
234
|
+
type=PydanticCustomError(
|
|
235
|
+
"value_error",
|
|
236
|
+
"The event must have a payload descriptor.",
|
|
237
|
+
),
|
|
238
|
+
loc=("payload_descriptor",),
|
|
239
|
+
input=self.payload_descriptor,
|
|
240
|
+
ctx={},
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if self.payload_descriptor is not None:
|
|
245
|
+
if len(self.payload_descriptor) != 1:
|
|
246
|
+
validation_errors.append(
|
|
247
|
+
InitErrorDetails(
|
|
248
|
+
type=PydanticCustomError(
|
|
249
|
+
"value_error",
|
|
250
|
+
"The event must have exactly one payload descriptor.",
|
|
251
|
+
),
|
|
252
|
+
loc=("payload_descriptor",),
|
|
253
|
+
input=self.payload_descriptor,
|
|
254
|
+
ctx={},
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
payload_descriptor = self.payload_descriptor[0]
|
|
259
|
+
|
|
260
|
+
if payload_descriptor.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
261
|
+
validation_errors.append(
|
|
262
|
+
InitErrorDetails(
|
|
263
|
+
type=PydanticCustomError(
|
|
264
|
+
"value_error",
|
|
265
|
+
"The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'.",
|
|
266
|
+
),
|
|
267
|
+
loc=("payload_descriptor",),
|
|
268
|
+
input=self.payload_descriptor,
|
|
269
|
+
ctx={},
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if payload_descriptor.units != "KW":
|
|
274
|
+
validation_errors.append(
|
|
275
|
+
InitErrorDetails(
|
|
276
|
+
type=PydanticCustomError(
|
|
277
|
+
"value_error",
|
|
278
|
+
"The payload descriptor must have a units of 'KW' (case sensitive).",
|
|
279
|
+
),
|
|
280
|
+
loc=("payload_descriptor",),
|
|
281
|
+
input=self.payload_descriptor,
|
|
282
|
+
ctx={},
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return self, validation_errors
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _event_interval_gac_compliant(self: Event) -> Tuple[Event, list[InitErrorDetails]]:
|
|
290
|
+
"""Enforces that the event interval is GAC compliant.
|
|
291
|
+
|
|
292
|
+
GAC enforces the following constraints for event intervals:
|
|
293
|
+
|
|
294
|
+
- The event interval must have an id value that is strictly increasing.
|
|
295
|
+
- The event interval must have exactly one payload.
|
|
296
|
+
- The payload of the event interval must have a type of 'IMPORT_CAPACITY_LIMIT'
|
|
297
|
+
"""
|
|
298
|
+
validation_errors: list[InitErrorDetails] = []
|
|
299
|
+
|
|
300
|
+
if not self.intervals:
|
|
301
|
+
validation_errors.append(
|
|
302
|
+
InitErrorDetails(
|
|
303
|
+
type=PydanticCustomError(
|
|
304
|
+
"value_error",
|
|
305
|
+
"The event must have at least one interval.",
|
|
306
|
+
),
|
|
307
|
+
loc=("intervals",),
|
|
308
|
+
input=self.intervals,
|
|
309
|
+
ctx={},
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
if not all(curr.id > prev.id for prev, curr in pairwise(self.intervals)):
|
|
314
|
+
validation_errors.append(
|
|
315
|
+
InitErrorDetails(
|
|
316
|
+
type=PydanticCustomError(
|
|
317
|
+
"value_error",
|
|
318
|
+
"The event interval must have an id value that is strictly increasing.",
|
|
319
|
+
),
|
|
320
|
+
loc=("intervals",),
|
|
321
|
+
input=self.intervals,
|
|
322
|
+
ctx={},
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
for interval in self.intervals:
|
|
327
|
+
if interval.payloads is None:
|
|
328
|
+
validation_errors.append(
|
|
329
|
+
InitErrorDetails(
|
|
330
|
+
type=PydanticCustomError(
|
|
331
|
+
"value_error",
|
|
332
|
+
"The event interval must have a payload.",
|
|
333
|
+
),
|
|
334
|
+
loc=("intervals",),
|
|
335
|
+
input=self.intervals,
|
|
336
|
+
ctx={},
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if len(interval.payloads) != 1:
|
|
341
|
+
validation_errors.append(
|
|
342
|
+
InitErrorDetails(
|
|
343
|
+
type=PydanticCustomError(
|
|
344
|
+
"value_error",
|
|
345
|
+
"The event interval must have exactly one payload.",
|
|
346
|
+
),
|
|
347
|
+
loc=("intervals",),
|
|
348
|
+
input=self.intervals,
|
|
349
|
+
ctx={},
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
else:
|
|
353
|
+
payload = interval.payloads[0]
|
|
354
|
+
|
|
355
|
+
if payload.type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
356
|
+
validation_errors.append(
|
|
357
|
+
InitErrorDetails(
|
|
358
|
+
type=PydanticCustomError(
|
|
359
|
+
"value_error",
|
|
360
|
+
"The event interval payload must have a payload type of 'IMPORT_CAPACITY_LIMIT'.",
|
|
361
|
+
),
|
|
362
|
+
loc=("intervals",),
|
|
363
|
+
input=self.intervals,
|
|
364
|
+
ctx={},
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
return self, validation_errors
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
@ValidatorRegistry.register(Event, ValidatorModel())
|
|
372
|
+
def event_gac_compliant(self: Event) -> Event:
|
|
373
|
+
"""Enforces that events are GAC compliant.
|
|
374
|
+
|
|
375
|
+
GAC enforces the following constraints for events:
|
|
376
|
+
|
|
377
|
+
- The event must not have a priority set.
|
|
378
|
+
- The event must have either a continuous or seperated interval definition.
|
|
379
|
+
"""
|
|
380
|
+
validation_errors: list[InitErrorDetails] = []
|
|
381
|
+
|
|
382
|
+
if self.priority is not None:
|
|
383
|
+
validation_errors.append(
|
|
384
|
+
InitErrorDetails(
|
|
385
|
+
type=PydanticCustomError(
|
|
386
|
+
"value_error",
|
|
387
|
+
"The event must not have a priority set for GAC 2.0 compliance",
|
|
388
|
+
),
|
|
389
|
+
loc=("priority",),
|
|
390
|
+
input=self.priority,
|
|
391
|
+
ctx={},
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
interval_periods_validated, errors = _continuous_or_seperated(self)
|
|
396
|
+
validation_errors.extend(errors)
|
|
397
|
+
|
|
398
|
+
targets_validated, errors = _targets_compliant(interval_periods_validated)
|
|
399
|
+
validation_errors.extend(errors)
|
|
400
|
+
|
|
401
|
+
payload_descriptor_validated, errors = _payload_descriptor_gac_compliant(
|
|
402
|
+
targets_validated
|
|
403
|
+
)
|
|
404
|
+
validation_errors.extend(errors)
|
|
405
|
+
|
|
406
|
+
event_interval_validated, errors = _event_interval_gac_compliant(
|
|
407
|
+
payload_descriptor_validated
|
|
408
|
+
)
|
|
409
|
+
validation_errors.extend(errors)
|
|
410
|
+
|
|
411
|
+
if validation_errors:
|
|
412
|
+
raise ValidationError.from_exception_data(
|
|
413
|
+
title=self.__class__.__name__, line_errors=validation_errors
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return event_interval_validated
|
openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/program_gac_compliant.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Module which implements GAC compliance validators for the program OpenADR3 types."""
|
|
2
|
+
|
|
3
|
+
from openadr3_client.models.program.program import Program
|
|
4
|
+
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@ValidatorRegistry.register(Program, ValidatorModel())
|
|
13
|
+
def program_gac_compliant(self: Program) -> Program:
|
|
14
|
+
"""Enforces that the program is GAC compliant.
|
|
15
|
+
|
|
16
|
+
GAC enforces the following constraints for programs:
|
|
17
|
+
- The program must have a retailer name
|
|
18
|
+
- The retailer name must be between 2 and 128 characters long.
|
|
19
|
+
- The program MUST have a programType.
|
|
20
|
+
- The programType MUST equal "DSO_CPO_INTERFACE-x.x.x, where x.x.x is the version as defined in the GAC specification.
|
|
21
|
+
- The program MUST have bindingEvents set to True.
|
|
22
|
+
are allowed there.
|
|
23
|
+
"""
|
|
24
|
+
validation_errors: list[InitErrorDetails] = []
|
|
25
|
+
|
|
26
|
+
program_type_regex = r"^DSO_CPO_INTERFACE-(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
|
27
|
+
|
|
28
|
+
if self.retailer_name is None:
|
|
29
|
+
validation_errors.append(
|
|
30
|
+
InitErrorDetails(
|
|
31
|
+
type=PydanticCustomError(
|
|
32
|
+
"value_error",
|
|
33
|
+
"The program must have a retailer name.",
|
|
34
|
+
),
|
|
35
|
+
loc=("retailer_name",),
|
|
36
|
+
input=self.retailer_name,
|
|
37
|
+
ctx={},
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if self.retailer_name is not None and (
|
|
42
|
+
len(self.retailer_name) < 2 or len(self.retailer_name) > 128
|
|
43
|
+
):
|
|
44
|
+
validation_errors.append(
|
|
45
|
+
InitErrorDetails(
|
|
46
|
+
type=PydanticCustomError(
|
|
47
|
+
"value_error",
|
|
48
|
+
"The retailer name must be between 2 and 128 characters long.",
|
|
49
|
+
),
|
|
50
|
+
loc=("retailer_name",),
|
|
51
|
+
input=self.retailer_name,
|
|
52
|
+
ctx={},
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if self.program_type is None:
|
|
57
|
+
validation_errors.append(
|
|
58
|
+
InitErrorDetails(
|
|
59
|
+
type=PydanticCustomError(
|
|
60
|
+
"value_error",
|
|
61
|
+
"The program must have a program type.",
|
|
62
|
+
),
|
|
63
|
+
loc=("program_type",),
|
|
64
|
+
input=self.program_type,
|
|
65
|
+
ctx={},
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
if self.program_type is not None and not re.fullmatch(
|
|
69
|
+
program_type_regex, self.program_type
|
|
70
|
+
):
|
|
71
|
+
validation_errors.append(
|
|
72
|
+
InitErrorDetails(
|
|
73
|
+
type=PydanticCustomError(
|
|
74
|
+
"value_error",
|
|
75
|
+
"The program type must follow the format DSO_CPO_INTERFACE-x.x.x.",
|
|
76
|
+
),
|
|
77
|
+
loc=("program_type",),
|
|
78
|
+
input=self.program_type,
|
|
79
|
+
ctx={},
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if self.binding_events is False:
|
|
84
|
+
validation_errors.append(
|
|
85
|
+
InitErrorDetails(
|
|
86
|
+
type=PydanticCustomError(
|
|
87
|
+
"value_error",
|
|
88
|
+
"The program must have bindingEvents set to True.",
|
|
89
|
+
),
|
|
90
|
+
loc=("binding_events",),
|
|
91
|
+
input=self.binding_events,
|
|
92
|
+
ctx={},
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if validation_errors:
|
|
97
|
+
raise ValidationError.from_exception_data(
|
|
98
|
+
title=self.__class__.__name__, line_errors=validation_errors
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return self
|
openadr3_client_gac_compliance-1.2.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
3
|
+
from openadr3_client.models.ven.ven import Ven
|
|
4
|
+
import pycountry
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
from pydantic_core import InitErrorDetails, PydanticCustomError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@ValidatorRegistry.register(Ven, ValidatorModel())
|
|
11
|
+
def ven_gac_compliant(self: Ven) -> Ven:
|
|
12
|
+
"""Enforces that the ven is GAC compliant.
|
|
13
|
+
|
|
14
|
+
GAC enforces the following constraints for vens:
|
|
15
|
+
- The ven must have a ven name
|
|
16
|
+
- The ven name must be an eMI3 identifier.
|
|
17
|
+
"""
|
|
18
|
+
validation_errors: list[InitErrorDetails] = []
|
|
19
|
+
|
|
20
|
+
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
21
|
+
|
|
22
|
+
if not re.fullmatch(emi3_identifier_regex, self.ven_name):
|
|
23
|
+
validation_errors.append(
|
|
24
|
+
InitErrorDetails(
|
|
25
|
+
type=PydanticCustomError(
|
|
26
|
+
"value_error",
|
|
27
|
+
"The ven name must be formatted as an eMI3 identifier.",
|
|
28
|
+
),
|
|
29
|
+
loc=("ven_name",),
|
|
30
|
+
input=self.ven_name,
|
|
31
|
+
ctx={},
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
alpha_2_country = pycountry.countries.get(alpha_2=self.ven_name[:2])
|
|
36
|
+
|
|
37
|
+
if alpha_2_country is None:
|
|
38
|
+
validation_errors.append(
|
|
39
|
+
InitErrorDetails(
|
|
40
|
+
type=PydanticCustomError(
|
|
41
|
+
"value_error",
|
|
42
|
+
"The first two characters of the ven name must be a valid ISO 3166-1 alpha-2 country code.",
|
|
43
|
+
),
|
|
44
|
+
loc=("ven_name",),
|
|
45
|
+
input=self.ven_name,
|
|
46
|
+
ctx={},
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if validation_errors:
|
|
51
|
+
raise ValidationError.from_exception_data(
|
|
52
|
+
title=self.__class__.__name__, line_errors=validation_errors
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return self
|
openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/event_gac_compliant.py
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
"""Module which implements GAC compliance validators for the event OpenADR3 types.
|
|
2
|
-
|
|
3
|
-
This module validates all the object constraints and requirements on the OpenADR3 events resource
|
|
4
|
-
as specified in the Grid aware charging (GAC) specification.
|
|
5
|
-
|
|
6
|
-
There is one requirement that is not validated here, as it cannot be validated through the scope of the
|
|
7
|
-
pydantic validators. Namely, the requirement that a safe mode event MUST be present in a program.
|
|
8
|
-
|
|
9
|
-
As the pydantic validator works on the scope of a single Event Object, it is not possible to validate
|
|
10
|
-
that a safe mode event is present in a program. And it cannot be validated on the Program object,
|
|
11
|
-
as the program object does not contain the events, these are stored seperately in the VTN.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from itertools import pairwise
|
|
15
|
-
import re
|
|
16
|
-
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
17
|
-
from openadr3_client.models.event.event import Event
|
|
18
|
-
from openadr3_client.models.event.event_payload import EventPayloadType
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def _continuous_or_seperated(self: Event) -> Event:
|
|
22
|
-
"""Enforces that events either have consistent interval definitions compliant with GAC.
|
|
23
|
-
|
|
24
|
-
the Grid aware charging (GAC) specification allows for two types of (mutually exclusive)
|
|
25
|
-
interval definitions:
|
|
26
|
-
|
|
27
|
-
1. Continuous
|
|
28
|
-
2. Seperated
|
|
29
|
-
|
|
30
|
-
The continious implementation can be used when all intervals have the same duration.
|
|
31
|
-
In this case, only the top-level intervalPeriod of the event can be used, and the intervalPeriods
|
|
32
|
-
of the individual intervals must be None.
|
|
33
|
-
|
|
34
|
-
In the seperated intervalDefinition approach, the intervalPeriods must be set on each individual intervals,
|
|
35
|
-
and the top-level intervalPeriod of the event must be None. This seperated approach is used when events have differing
|
|
36
|
-
durations.
|
|
37
|
-
"""
|
|
38
|
-
intervals = self.intervals or ()
|
|
39
|
-
|
|
40
|
-
if self.interval_period is None:
|
|
41
|
-
# interval period not set at top level of the event.
|
|
42
|
-
# Ensure that all intervals have the interval_period defined, to comply with the GAC specification.
|
|
43
|
-
undefined_intervals_period = [i for i in intervals if i.interval_period is None]
|
|
44
|
-
if undefined_intervals_period:
|
|
45
|
-
raise ValueError(
|
|
46
|
-
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'."
|
|
47
|
-
)
|
|
48
|
-
else:
|
|
49
|
-
# interval period set at top level of the event.
|
|
50
|
-
# Ensure that all intervals do not have the interval_period defined, to comply with the GAC specification.
|
|
51
|
-
duplicate_interval_period = [
|
|
52
|
-
i for i in intervals if i.interval_period is not None
|
|
53
|
-
]
|
|
54
|
-
if duplicate_interval_period:
|
|
55
|
-
raise ValueError(
|
|
56
|
-
"Either 'interval_period' must be set on the event once, or every interval must have its own 'interval_period'."
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
return self
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _targets_compliant(self: Event) -> Event:
|
|
63
|
-
"""Enforces that the targets of the event are compliant with GAC.
|
|
64
|
-
|
|
65
|
-
GAC enforces the following constraints for targets:
|
|
66
|
-
|
|
67
|
-
- The event must contain a POWER_SERVICE_LOCATIONS target.
|
|
68
|
-
- The POWER_SERVICE_LOCATIONS target value must be a list of 'EAN18' values.
|
|
69
|
-
- The event must contain a VEN_NAME target.
|
|
70
|
-
- The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters).
|
|
71
|
-
"""
|
|
72
|
-
targets = self.targets or ()
|
|
73
|
-
|
|
74
|
-
power_service_locations = [
|
|
75
|
-
t for t in targets if t.type == "POWER_SERVICE_LOCATIONS"
|
|
76
|
-
]
|
|
77
|
-
ven_names = [t for t in targets if t.type == "VEN_NAME"]
|
|
78
|
-
|
|
79
|
-
if not power_service_locations:
|
|
80
|
-
raise ValueError("The event must contain a POWER_SERVICE_LOCATIONS target.")
|
|
81
|
-
|
|
82
|
-
if not ven_names:
|
|
83
|
-
raise ValueError("The event must contain a VEN_NAME target.")
|
|
84
|
-
|
|
85
|
-
if len(power_service_locations) > 1:
|
|
86
|
-
raise ValueError(
|
|
87
|
-
"The event must contain exactly one POWER_SERVICE_LOCATIONS target."
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
if len(ven_names) > 1:
|
|
91
|
-
raise ValueError("The event must contain only one VEN_NAME target.")
|
|
92
|
-
|
|
93
|
-
power_service_location = power_service_locations[0]
|
|
94
|
-
ven_name = ven_names[0]
|
|
95
|
-
|
|
96
|
-
if len(power_service_location.values) == 0:
|
|
97
|
-
raise ValueError("The POWER_SERVICE_LOCATIONS target value cannot be empty.")
|
|
98
|
-
|
|
99
|
-
if not all(re.fullmatch(r"\d{18}", v) for v in power_service_location.values):
|
|
100
|
-
raise ValueError(
|
|
101
|
-
"The POWER_SERVICE_LOCATIONS target value must be a list of 'EAN18' values."
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
if len(ven_name.values) == 0:
|
|
105
|
-
raise ValueError("The VEN_NAME target value cannot be empty.")
|
|
106
|
-
|
|
107
|
-
if not all(1 <= len(v) <= 128 for v in ven_name.values):
|
|
108
|
-
raise ValueError(
|
|
109
|
-
"The VEN_NAME target value must be a list of 'ven object name' values (between 1 and 128 characters)."
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
return self
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _payload_descriptor_gac_compliant(self: Event) -> Event:
|
|
116
|
-
"""Enforces that the payload descriptor is GAC compliant.
|
|
117
|
-
|
|
118
|
-
GAC enforces the following constraints for payload descriptors:
|
|
119
|
-
|
|
120
|
-
- The event interval must exactly one payload descriptor.
|
|
121
|
-
- The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'
|
|
122
|
-
- The payload descriptor must have a units of 'KW' (case sensitive).
|
|
123
|
-
"""
|
|
124
|
-
if self.payload_descriptor is None:
|
|
125
|
-
raise ValueError("The event must have a payload descriptor.")
|
|
126
|
-
|
|
127
|
-
if len(self.payload_descriptor) != 1:
|
|
128
|
-
raise ValueError("The event must have exactly one payload descriptor.")
|
|
129
|
-
|
|
130
|
-
payload_descriptor = self.payload_descriptor[0]
|
|
131
|
-
|
|
132
|
-
if payload_descriptor.payload_type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
133
|
-
raise ValueError(
|
|
134
|
-
"The payload descriptor must have a payload type of 'IMPORT_CAPACITY_LIMIT'."
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
if payload_descriptor.units != "KW":
|
|
138
|
-
raise ValueError(
|
|
139
|
-
"The payload descriptor must have a units of 'KW' (case sensitive)."
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
return self
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def _event_interval_gac_compliant(self: Event) -> Event:
|
|
146
|
-
"""Enforces that the event interval is GAC compliant.
|
|
147
|
-
|
|
148
|
-
GAC enforces the following constraints for event intervals:
|
|
149
|
-
|
|
150
|
-
- The event interval must have an id value that is strictly increasing.
|
|
151
|
-
- The event interval must have exactly one payload.
|
|
152
|
-
- The payload of the event interval must have a type of 'IMPORT_CAPACITY_LIMIT'
|
|
153
|
-
"""
|
|
154
|
-
if not self.intervals:
|
|
155
|
-
raise ValueError("The event must have at least one interval.")
|
|
156
|
-
|
|
157
|
-
if not all(curr.id > prev.id for prev, curr in pairwise(self.intervals)):
|
|
158
|
-
raise ValueError(
|
|
159
|
-
"The event interval must have an id value that is strictly increasing."
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
for interval in self.intervals:
|
|
163
|
-
if interval.payloads is None:
|
|
164
|
-
raise ValueError("The event interval must have a payload.")
|
|
165
|
-
|
|
166
|
-
if len(interval.payloads) != 1:
|
|
167
|
-
raise ValueError("The event interval must have exactly one payload.")
|
|
168
|
-
|
|
169
|
-
payload = interval.payloads[0]
|
|
170
|
-
|
|
171
|
-
if payload.type != EventPayloadType.IMPORT_CAPACITY_LIMIT:
|
|
172
|
-
raise ValueError(
|
|
173
|
-
"The event interval payload must have a payload type of 'IMPORT_CAPACITY_LIMIT'."
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
return self
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
@ValidatorRegistry.register(Event, ValidatorModel())
|
|
180
|
-
def event_gac_compliant(self: Event) -> Event:
|
|
181
|
-
"""Enforces that events are GAC compliant.
|
|
182
|
-
|
|
183
|
-
GAC enforces the following constraints for events:
|
|
184
|
-
|
|
185
|
-
- The event must not have a priority set.
|
|
186
|
-
- The event must have either a continuous or seperated interval definition.
|
|
187
|
-
"""
|
|
188
|
-
if self.priority is not None:
|
|
189
|
-
raise ValueError(
|
|
190
|
-
"The event must not have a priority set for GAC 2.0 compliance"
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
interval_periods_validated = _continuous_or_seperated(self)
|
|
194
|
-
targets_validated = _targets_compliant(interval_periods_validated)
|
|
195
|
-
payload_descriptor_validated = _payload_descriptor_gac_compliant(targets_validated)
|
|
196
|
-
event_interval_validated = _event_interval_gac_compliant(
|
|
197
|
-
payload_descriptor_validated
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
return event_interval_validated
|
openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/program_gac_compliant.py
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"""Module which implements GAC compliance validators for the program OpenADR3 types."""
|
|
2
|
-
|
|
3
|
-
from openadr3_client.models.program.program import Program
|
|
4
|
-
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
5
|
-
|
|
6
|
-
import re
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
@ValidatorRegistry.register(Program, ValidatorModel())
|
|
10
|
-
def program_gac_compliant(self: Program) -> Program:
|
|
11
|
-
"""Enforces that the program is GAC compliant.
|
|
12
|
-
|
|
13
|
-
GAC enforces the following constraints for programs:
|
|
14
|
-
- The program must have a retailer name
|
|
15
|
-
- The retailer name must be between 2 and 128 characters long.
|
|
16
|
-
- The program MUST have a programType.
|
|
17
|
-
- The programType MUST equal "DSO_CPO_INTERFACE-x.x.x, where x.x.x is the version as defined in the GAC specification.
|
|
18
|
-
- The program MUST have bindingEvents set to True.
|
|
19
|
-
are allowed there.
|
|
20
|
-
"""
|
|
21
|
-
program_type_regex = r"^DSO_CPO_INTERFACE-(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
|
22
|
-
|
|
23
|
-
if self.retailer_name is None:
|
|
24
|
-
raise ValueError("The program must have a retailer name.")
|
|
25
|
-
|
|
26
|
-
if len(self.retailer_name) < 2 or len(self.retailer_name) > 128:
|
|
27
|
-
raise ValueError("The retailer name must be between 2 and 128 characters long.")
|
|
28
|
-
|
|
29
|
-
if self.program_type is None:
|
|
30
|
-
raise ValueError("The program must have a program type.")
|
|
31
|
-
if not re.fullmatch(program_type_regex, self.program_type):
|
|
32
|
-
raise ValueError(
|
|
33
|
-
"The program type must follow the format DSO_CPO_INTERFACE-x.x.x."
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
if self.binding_events is False:
|
|
37
|
-
raise ValueError("The program must have bindingEvents set to True.")
|
|
38
|
-
|
|
39
|
-
return self
|
openadr3_client_gac_compliance-1.1.0/openadr3_client_gac_compliance/gac20/ven_gac_compliant.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
from openadr3_client.models.model import ValidatorRegistry, Model as ValidatorModel
|
|
3
|
-
from openadr3_client.models.ven.ven import Ven
|
|
4
|
-
import pycountry
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@ValidatorRegistry.register(Ven, ValidatorModel())
|
|
8
|
-
def ven_gac_compliant(self: Ven) -> Ven:
|
|
9
|
-
"""Enforces that the ven is GAC compliant.
|
|
10
|
-
|
|
11
|
-
GAC enforces the following constraints for vens:
|
|
12
|
-
- The ven must have a ven name
|
|
13
|
-
- The ven name must be an eMI3 identifier.
|
|
14
|
-
"""
|
|
15
|
-
emi3_identifier_regex = r"^[A-Z]{2}-?[A-Z0-9]{3}$"
|
|
16
|
-
|
|
17
|
-
if not re.fullmatch(emi3_identifier_regex, self.ven_name):
|
|
18
|
-
raise ValueError("The ven name must be formatted as an eMI3 identifier.")
|
|
19
|
-
|
|
20
|
-
alpha_2_country = pycountry.countries.get(alpha_2=self.ven_name[:2])
|
|
21
|
-
|
|
22
|
-
if alpha_2_country is None:
|
|
23
|
-
raise ValueError(
|
|
24
|
-
"The first two characters of the ven name must be a valid ISO 3166-1 alpha-2 country code."
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
return self
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|