diff --git a/src/python/otsim/ieee_2030_5/README.md b/src/python/otsim/ieee_2030_5/README.md new file mode 100644 index 0000000..a92810d --- /dev/null +++ b/src/python/otsim/ieee_2030_5/README.md @@ -0,0 +1 @@ +# SunSpec 2030.5 Client \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/__init__.py b/src/python/otsim/ieee_2030_5/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/python/otsim/ieee_2030_5/client.py b/src/python/otsim/ieee_2030_5/client.py new file mode 100644 index 0000000..5b1b5d7 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client.py @@ -0,0 +1,1052 @@ + +import sys, typing +import xml.etree.ElementTree as ET + +import threading +import json +import re +import ssl +import subprocess +import sys +from dataclasses import replace +from http.client import HTTPConnection, HTTPSConnection +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple +from otsim.ieee_2030_5.client_helper.client import IEEE2030_5_Client +from otsim.ieee_2030_5.client_helper.models.enums import DeviceCategoryType +from otsim.ieee_2030_5.constants import TypeConstants + +import otsim.ieee_2030_5.client_helper.models as m +import time + +from otsim.msgbus import envelope +from otsim.msgbus.envelope import Envelope +from otsim.msgbus.pusher import Pusher +from otsim.msgbus.subscriber import Subscriber + +class IEEE20305Client(): + EXPECTED_WRITABLE_CONTROL_TAGS = { + 'pv:setpoint', + 'storage:setchargedischargerate', + } + + def __init__(self, pub: str, pull: str, el: ET.Element, config_dir: Optional[Path] = None): + self.name = el.get('name', default='ot-sim-20305-client') + + self.pub = pub + self.pull = pull + + self.device_id = el.findtext('device-id') + + self.cert_dir = Path(el.findtext('certificate-directory')) + self.server_address = el.findtext('server-address') + self.server_port = el.findtext('server-port') + self.config_dir = config_dir or Path.cwd() + + self.site_name = el.findtext('site-name') + self.device_name = el.findtext('device-name') or None + self.polling_rate = int(el.findtext('polling-rate-seconds')) + self.control_setpoint_tag = el.findtext('control-setpoint-tag', default='der.active-power-setpoint') + + self.device_categories = [] + for cat in el.findall('category'): + if cat.text: + self.device_categories.append(cat.text) + + self.device_category_bitmap_hex: Optional[str] = self._build_device_category_bitmap() + self.device_category_bitmap_int: Optional[int] = ( + int(self.device_category_bitmap_hex, 16) + if self.device_category_bitmap_hex + else None + ) + + + self.readings = [] + self.readings_by_tag: Dict[str, Dict[str, Any]] = {} + self.local_state: Dict[str, int] = {} + for i, elm in enumerate(el.findall('reading')): + tag = elm.findtext('tag') + local_id = (i + 1).to_bytes(2, 'big') + multiplier_text = elm.findtext('power-of-ten-multiplier') + multiplier = int(multiplier_text) if multiplier_text else 0 + reading = { + "description": elm.findtext('description'), + "type": elm.findtext('reading-type'), + "tag": tag, + "mrid": "", + "local_id": local_id, + "power_of_ten_multiplier": multiplier, + } + self.readings.append(reading) + self.readings_by_tag[self._normalize_tag(tag)] = reading + self.local_state[self._normalize_tag(tag)] = 0 + + self.allowed_control_setpoint_tags = set(self.EXPECTED_WRITABLE_CONTROL_TAGS) + self.control_setpoint_tags = self._resolve_control_setpoint_tags(el) + + self._acknowledged_control_mrid: Optional[bytes] = None + self._resolved_device_href: Optional[str] = None + self._logged_missing_setpoint_tags = False + self._curve_cache: Dict[str, Any] = {} + self._ramp_state_by_tag: Dict[str, Dict[str, float]] = {} + + self.subscriber = Subscriber(pub) + self.pusher = Pusher(pull) + + self.running = False + self.log("adding msgbus update and status listeners") + self.subscriber.add_update_handler(self.listen_msgbus_updates) + self.subscriber.add_status_handler(self.listen_msgbus_status) + + def log(self, msg): + print(f'[IEEE 2030.5 Client] {msg}', flush=True) + + @staticmethod + def _normalize_tag(tag: str) -> str: + return (tag or '').replace('_', '-').strip().lower() + + def _resolve_control_setpoint_tags(self, el: ET.Element) -> List[str]: + self.log( + 'Allowed writable control setpoints: ' + + ', '.join(sorted(self.allowed_control_setpoint_tags)) + ) + configured: List[str] = [] + for node in el.findall('control-setpoint-tag'): + if not node.text: + continue + configured.extend([part.strip() for part in node.text.split(',') if part.strip()]) + + if not configured and self.control_setpoint_tag: + configured = [self.control_setpoint_tag] + + reading_tag_lookup: Dict[str, str] = { + self._normalize_tag(reading.get('tag', '')): (reading.get('tag') or '') + for reading in self.readings + if reading.get('tag') + } + + resolved: List[str] = [] + seen: Set[str] = set() + + def _try_add(tag: str) -> None: + normalized = self._normalize_tag(tag) + if not normalized or normalized in seen: + return + if self.allowed_control_setpoint_tags and normalized not in self.allowed_control_setpoint_tags: + return + if normalized not in reading_tag_lookup: + return + seen.add(normalized) + resolved.append(reading_tag_lookup[normalized]) + + for tag in configured: + _try_add(tag) + + # Auto-select writable tags from readings when explicit config did not resolve. + if not resolved and self.allowed_control_setpoint_tags: + for normalized, original in reading_tag_lookup.items(): + if normalized in self.allowed_control_setpoint_tags and normalized not in seen: + seen.add(normalized) + resolved.append(original) + + if resolved: + self.log(f'Control setpoint publish tags: {", ".join(resolved)}') + else: + self.log('No valid writable control setpoint tags resolved; control outputs will not be published') + + return resolved + + def _reading_type_for_name(self, name: str, multiplier: int = 0): + base_type = None + match name: + case "active-power": + base_type = TypeConstants.ACTIVE_POWER + case "reactive-power": + base_type = TypeConstants.REACTIVE_POWER + case "apparent-power": + base_type = TypeConstants.APPARENT_POWER + case "voltage": + base_type = TypeConstants.VOLTAGE + case "current": + base_type = TypeConstants.CURRENT + case "frequency": + base_type = TypeConstants.FREQUENCY + case "energy-exported": + base_type = TypeConstants.ENERGY_EXPORTED + case "percentage": + base_type = TypeConstants.PERCENTAGE + case _: + self.log(f"Unsupported reading type '{name}', defaulting to ACTIVE_POWER") + base_type = TypeConstants.ACTIVE_POWER + + return replace(base_type, powerOfTenMultiplier=multiplier) + + @staticmethod + def _scale_curve_axis(value: Any, multiplier: Any) -> Optional[float]: + if value is None: + return None + try: + numeric = float(value) + power = int(multiplier or 0) + return numeric * (10 ** power) + except (TypeError, ValueError): + return None + + def _lookup_measurement(self, reading_type: str, output_tag: Optional[str]) -> Optional[float]: + desired_domain = None + if output_tag and ':' in output_tag: + desired_domain = output_tag.split(':', 1)[0].strip().lower() + + preferred = None + fallback = None + for reading in self.readings: + if reading.get('type') != reading_type: + continue + raw_tag = reading.get('tag') or '' + normalized_tag = self._normalize_tag(raw_tag) + raw_value = self.local_state.get(normalized_tag) + if raw_value is None: + continue + scaled = self._scale_curve_axis(raw_value, reading.get('power_of_ten_multiplier', 0)) + if scaled is None: + continue + fallback = scaled + if desired_domain and raw_tag.lower().startswith(f'{desired_domain}:'): + preferred = scaled + break + + return preferred if preferred is not None else fallback + + @staticmethod + def _piecewise_linear(points: List[Tuple[float, float]], x_value: float) -> Optional[float]: + if not points: + return None + + ordered = sorted(points, key=lambda item: item[0]) + if x_value <= ordered[0][0]: + return ordered[0][1] + if x_value >= ordered[-1][0]: + return ordered[-1][1] + + for idx in range(1, len(ordered)): + x0, y0 = ordered[idx - 1] + x1, y1 = ordered[idx] + if x0 <= x_value <= x1: + if abs(x1 - x0) < 1e-12: + return y1 + ratio = (x_value - x0) / (x1 - x0) + return y0 + ((y1 - y0) * ratio) + return ordered[-1][1] + + @staticmethod + def _percent_to_control_hundredths(percent_value: float) -> float: + # opModFixedW/MaxLimW values are represented in hundredths-of-percent. + if abs(percent_value) <= 100.0: + return percent_value * 100.0 + return percent_value + + def _curve_target_setpoint_w(self, control_base: Any, output_tag: Optional[str]) -> Optional[float]: + # Active power oriented curve modes supported by this client path. + curve_modes: List[Tuple[str, str]] = [ + ('opModVoltWatt', 'voltage'), + ('opModFreqWatt', 'frequency'), + ] + + for curve_field, reading_type in curve_modes: + curve_link = getattr(control_base, curve_field, None) + if curve_link is None: + continue + + curve = self._get_curve_from_link(curve_link) + if curve is None: + continue + + measured_x = self._lookup_measurement(reading_type=reading_type, output_tag=output_tag) + if measured_x is None: + continue + + if reading_type == 'voltage': + v_ref = getattr(curve, 'vRef', None) + if isinstance(v_ref, int) and v_ref > 0: + measured_x = (measured_x / float(v_ref)) * 100.0 + + x_multiplier = getattr(curve, 'xMultiplier', 0) + y_multiplier = getattr(curve, 'yMultiplier', 0) + points: List[Tuple[float, float]] = [] + for point in (getattr(curve, 'CurveData', []) or []): + x_scaled = self._scale_curve_axis(getattr(point, 'xvalue', None), x_multiplier) + y_scaled = self._scale_curve_axis(getattr(point, 'yvalue', None), y_multiplier) + if x_scaled is None or y_scaled is None: + continue + points.append((x_scaled, y_scaled)) + + if not points: + continue + + y_output = self._piecewise_linear(points, measured_x) + if y_output is None: + continue + return self._percent_to_control_hundredths(y_output) + + return None + + def _extract_control_setpoint_w(self, selected_control: Any, output_tag: Optional[str] = None) -> Optional[float]: + control_base = getattr(selected_control, 'DERControlBase', None) + if control_base is None: + return None + + fixed_w = getattr(control_base, 'opModFixedW', None) + if fixed_w is not None: + return float(fixed_w) + + max_lim_w = getattr(control_base, 'opModMaxLimW', None) + if max_lim_w is not None: + return float(max_lim_w) + + target_w = getattr(control_base, 'opModTargetW', None) + if target_w is not None: + value = getattr(target_w, 'value', None) + multiplier = getattr(target_w, 'multiplier', 0) or 0 + if value is not None: + return float(value) * (10 ** multiplier) + + # If direct power setpoints are absent, evaluate active-power curves. + curve_target = self._curve_target_setpoint_w(control_base, output_tag=output_tag) + if curve_target is not None: + return curve_target + + return None + + @staticmethod + def _control_identifier(control: Any) -> str: + mrid = getattr(control, 'mRID', None) + if isinstance(mrid, (bytes, bytearray)): + return mrid.hex() + if mrid: + return str(mrid) + href = getattr(control, 'href', None) + return str(href or 'default') + + def _get_curve_from_link(self, curve_link: Any) -> Optional[Any]: + href = getattr(curve_link, 'href', None) + if not href: + return None + if href in self._curve_cache: + return self._curve_cache[href] + try: + curve = self.client.request(href) + self._curve_cache[href] = curve + return curve + except Exception as exc: + self.log(f'Unable to fetch DERCurve {href}: {exc}') + return None + + def _find_curve_ramp_seconds(self, control_base: Any, increasing: bool) -> Optional[float]: + if control_base is None: + return None + + curve_fields = ( + 'opModVoltWatt', + 'opModWattVar', + 'opModFreqWatt', + 'opModVoltVar', + 'opModWattPF', + ) + + for field_name in curve_fields: + curve_link = getattr(control_base, field_name, None) + if curve_link is None: + continue + curve = self._get_curve_from_link(curve_link) + if curve is None: + continue + + preferred = getattr(curve, 'rampIncTms' if increasing else 'rampDecTms', None) + if isinstance(preferred, int) and preferred > 0: + return preferred / 100.0 + + fallback = getattr(curve, 'rampPT1Tms', None) + if isinstance(fallback, int) and fallback > 0: + return fallback / 100.0 + + return None + + @staticmethod + def _derive_gradient_ramp_seconds(from_value: float, to_value: float, set_grad_w: Any) -> Optional[float]: + if set_grad_w is None: + return None + try: + grad = float(set_grad_w) + except (TypeError, ValueError): + return None + if grad <= 0: + return None + + # setGradW is in hundredths-of-percent per second and opModFixedW is + # represented in hundredths-of-percent, so delta/grad yields seconds. + delta = abs(to_value - from_value) + return delta / grad + + def _resolve_transition_seconds( + self, + selected_control: Any, + default_control: Any, + from_value: float, + to_value: float, + ) -> float: + if abs(to_value - from_value) < 1e-9: + return 0.0 + + control_base = getattr(selected_control, 'DERControlBase', None) + + ramp_tms = getattr(control_base, 'rampTms', None) + if isinstance(ramp_tms, int) and ramp_tms > 0: + return ramp_tms / 100.0 + + curve_seconds = self._find_curve_ramp_seconds( + control_base, + increasing=(to_value >= from_value), + ) + if curve_seconds is not None: + return curve_seconds + + for source in (selected_control, default_control): + service_ramp = getattr(source, 'setESRampTms', None) + if isinstance(service_ramp, int) and service_ramp > 0: + return service_ramp / 100.0 + + grad_seconds = self._derive_gradient_ramp_seconds( + from_value=from_value, + to_value=to_value, + set_grad_w=getattr(source, 'setGradW', None), + ) + if grad_seconds is not None: + return grad_seconds + + soft_grad_seconds = self._derive_gradient_ramp_seconds( + from_value=from_value, + to_value=to_value, + set_grad_w=getattr(source, 'setSoftGradW', None), + ) + if soft_grad_seconds is not None: + return soft_grad_seconds + + return 0.0 + + def _ramped_setpoint_value( + self, + tag: str, + selected_control: Any, + default_control: Any, + target_value: float, + now_epoch: int, + ) -> float: + key = self._normalize_tag(tag) + state = self._ramp_state_by_tag.get(key) + control_id = self._control_identifier(selected_control) + + if state is None: + start_value = float(self.local_state.get(key, target_value)) + transition_seconds = self._resolve_transition_seconds( + selected_control=selected_control, + default_control=default_control, + from_value=start_value, + to_value=target_value, + ) + state = { + 'control_id': control_id, + 'start': start_value, + 'target': target_value, + 'start_ts': float(now_epoch), + 'transition_seconds': transition_seconds, + 'last_value': start_value, + } + self._ramp_state_by_tag[key] = state + + changed_instruction = ( + state.get('control_id') != control_id + or abs(float(state.get('target', 0.0)) - target_value) > 1e-9 + ) + + if changed_instruction: + current_value = float(state.get('last_value', state.get('target', target_value))) + transition_seconds = self._resolve_transition_seconds( + selected_control=selected_control, + default_control=default_control, + from_value=current_value, + to_value=target_value, + ) + state.update({ + 'control_id': control_id, + 'start': current_value, + 'target': target_value, + 'start_ts': float(now_epoch), + 'transition_seconds': transition_seconds, + }) + + transition_seconds = float(state.get('transition_seconds', 0.0) or 0.0) + start_value = float(state.get('start', target_value)) + end_value = float(state.get('target', target_value)) + + if transition_seconds <= 0.0: + value = end_value + else: + elapsed = max(0.0, float(now_epoch) - float(state.get('start_ts', now_epoch))) + ratio = min(1.0, elapsed / transition_seconds) + value = start_value + ((end_value - start_value) * ratio) + + state['last_value'] = value + self.local_state[key] = int(round(value)) + return value + + def _build_device_category_bitmap(self) -> Optional[str]: + """Convert device category names to a 4-byte hex bitmap. + + Returns hex string like '00800000' for COMBINED_PV_AND_STORAGE (bit 23), + or None if no categories are configured. + """ + if not self.device_categories: + return None + + bitmap = 0 + for cat_name in self.device_categories: + try: + cat_enum = DeviceCategoryType[cat_name] + bitmap |= (1 << cat_enum.value) + self.log(f"Added category {cat_name} (bit {cat_enum.value})") + except KeyError: + self.log(f"WARNING: Unknown device category '{cat_name}'") + + # Convert to 4-byte big-endian hex + hex_bitmap = format(bitmap, '08x') + self.log(f"Device category bitmap: {hex_bitmap}") + return hex_bitmap + + def generate_private_key(self, device_id: str, output_dir: Path) -> Path: + """Generate an EC (prime256v1) private key and return its path.""" + key_file = output_dir / f"{device_id}.key" + if key_file.exists(): + self.log(f"Private key already exists: {str(key_file)}") + return key_file + + self.log(f"Generating private key for {device_id}") + result = subprocess.run( + ["openssl", "ecparam", "-genkey", "-name", "prime256v1", "-out", str(key_file)], + capture_output=True, text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"openssl ecparam failed: {result.stderr}") + self.log(f" -> {str(key_file)}") + return key_file + + def generate_csr(self, device_id: str, key_file: Path, output_dir: Path) -> Path: + """Generate a CSR with *device_id* as the Common Name.""" + csr_file = output_dir / f"{device_id}.csr" + if csr_file.exists(): + self.log(f"CSR already exists: {str(csr_file)}") + return csr_file + + self.log(f"Generating CSR for {device_id}") + result = subprocess.run( + ["openssl", "req", "-new", "-key", str(key_file), + "-out", str(csr_file), "-subj", f"/CN={device_id}"], + capture_output=True, text=True, + ) + if result.returncode != 0: + raise RuntimeError(f"openssl req failed: {result.stderr}") + self.log(f" -> {str(csr_file)}") + return csr_file + + def submit_csr(self, device_id: str, csr_file: Path, + server: str, port: int, + use_https: bool = False) -> dict: + """POST the CSR to ``/api/csr/submit`` and return the JSON response. + + The returned dict has at least: + ``certificate``, ``ca_certificate``, ``lfdi``, ``sfdi``. + """ + self.log(f"Submitting CSR to {server}:{port}") + payload = json.dumps({"device_id": device_id, "csr": csr_file.read_text()}) + + if use_https: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + conn = HTTPSConnection(server, port, context=ctx) + else: + conn = HTTPConnection(server, port) + while True: + try: + conn.request( + "POST", "/api/csr/submit", + body=payload, + headers={"Content-Type": "application/json", + "Content-Length": str(len(payload))}, + ) + resp = conn.getresponse() + body = resp.read().decode("utf-8") + + if resp.status != 200: + raise RuntimeError(f"Server returned {resp.status}: {body}") + + data = json.loads(body) + if not data.get("success"): + raise RuntimeError(f"Server error: {data.get('error')}") + self.log(f"Certificate signed - LFDI={data['lfdi']} SFDI={data['sfdi']}") + return data + except Exception: + continue + finally: + conn.close() + + def save_certificates(self, device_id: str, cert_data: dict, + output_dir: Path) -> Tuple[Path, Path]: + """Write signed cert and CA cert to *output_dir*.""" + cert_file = output_dir / f"{device_id}.crt" + ca_file = output_dir / "ca.crt" + + cert_file.write_text(cert_data["certificate"]) + ca_file.write_text(cert_data["ca_certificate"]) + + self.log(f"Certificate saved: {cert_file}") + self.log(f"CA cert saved: {ca_file}") + return cert_file, ca_file + + def _ssl_ctx(self, cert_file: Path, key_file: Path, ca_file: Path, + include_client_cert: bool = True) -> ssl.SSLContext: + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_OPTIONAL + ctx.load_verify_locations(cafile=str(ca_file)) + if include_client_cert: + ctx.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file)) + return ctx + + def _is_certificate_unknown(self, exc: ssl.SSLError) -> bool: + return "CERTIFICATE_UNKNOWN" in str(exc).upper() + + def register_device(self, cert_file: Path, key_file: Path, ca_file: Path, + server: str, port: int, sfdi: int, + name: Optional[str] = None) -> str: + """POST an ```` to ``/api/register`` (DMZ). + + Returns the ``Location`` header value (e.g. ``/edev/0``). + """ + self.log(f"Registering device via DMZ on {server}:{port}") + + # Build the registration XML with device category bitmap + dev_cat_hex = self.device_category_bitmap_hex + if dev_cat_hex: + body = ( + '' + f"{sfdi}" + f"{dev_cat_hex}" + "" + ) + else: + body = ( + '' + f"{sfdi}" + "" + ) + + # Build query string — site and optional friendly name + params: list[str] = [] + if self.site_name: + params.append(f"site={self.site_name}") + if name: + import urllib.parse + params.append(f"name={urllib.parse.quote(name, safe='')}") + path = "/api/register" + ("?" + "&".join(params) if params else "") + + for include_client_cert in (True, False): + ctx = self._ssl_ctx(cert_file, key_file, ca_file, include_client_cert=include_client_cert) + conn = HTTPSConnection(server, port, context=ctx) + try: + conn.request( + "POST", path, + body=body, + headers={"Content-Type": "application/sep+xml", + "Content-Length": str(len(body))}, + ) + resp = conn.getresponse() + resp_body = resp.read().decode("utf-8") + + if resp.status not in (200, 201): + raise RuntimeError(f"Registration failed ({resp.status}): {resp_body}") + + location = resp.headers.get("Location", "") + self.log(f"Device registered - Location: {location}") + return location + except ssl.SSLError as exc: + if include_client_cert and self._is_certificate_unknown(exc): + self.log("Server rejected the presented client cert during DMZ registration; retrying without a client cert") + continue + raise + finally: + conn.close() + + raise RuntimeError("Registration failed without a usable TLS mode") + + def verify_registration(self, cert_file: Path, key_file: Path, ca_file: Path, + server: str, port: int, + device_href: str, + pin: Optional[int] = None) -> Tuple[bool, Optional[int]]: + """Fetch the Registration resource and check the PIN. + + Returns ``(verified, server_pin)``. + """ + self.log("Verifying registration") + href_candidates = [f"{device_href}/rg"] + if "_" in device_href: + href_candidates.append(f"{device_href}_rg") + + for reg_href in href_candidates: + for include_client_cert in (True, False): + ctx = self._ssl_ctx(cert_file, key_file, ca_file, include_client_cert=include_client_cert) + conn = HTTPSConnection(server, port, context=ctx) + try: + conn.request("GET", reg_href) + resp = conn.getresponse() + reg_data = resp.read().decode("utf-8") + + if resp.status != 200: + continue + + match = re.search(r"(\d+)", reg_data) + server_pin: Optional[int] = int(match.group(1)) if match else None + + if server_pin is None: + self.log(" not found in registration response") + return False, None + + if pin is not None: + ok = server_pin == pin + if ok: + self.log("PIN verified") + else: + self.log(f"PIN mismatch - expected {pin}, got {server_pin}") + return ok, server_pin + + self.log(f"Server-assigned PIN: {server_pin}") + return True, server_pin + except ssl.SSLError as exc: + if include_client_cert and self._is_certificate_unknown(exc): + self.log("Server rejected the presented client cert while verifying registration; retrying without a client cert") + continue + raise + finally: + conn.close() + + self.log("Could not fetch registration") + return False, None + + def initialize_client(self): + # 1. Create key + self.key = self.generate_private_key(self.device_id, self.cert_dir) + # 2. Create CSR + self.csr_file = self.generate_csr(self.device_id, self.key, self.cert_dir) + # 3. Submit CSR to server + cert_data = self.submit_csr(self.device_id, self.csr_file, self.server_address, self.server_port, True) + # 4. Save certs + self.cert, self.ca_cert = self.save_certificates(self.device_id, cert_data, self.cert_dir) + # 5. Register via DMZ + self.device_url = self.register_device( + self.cert, self.key, self.ca_cert, + self.server_address, self.server_port, + cert_data["sfdi"], + name=self.device_name, + ) + # 6. Verify registration + verified, self.pin = self.verify_registration(self.cert, self.key, self.ca_cert, self.server_address, self.server_port, self.device_url) + + self.lfdi = cert_data["lfdi"] + self.sfdi = cert_data["sfdi"] + + self.log(f"""\nSUMMARY +Device ID: {self.device_id} +LFDI: {self.lfdi} +SFDI: {self.sfdi} +Key file: {self.key} +CSR file: {self.csr_file} +Certificate file: {self.cert} +Certificate Authority file: {self.ca_cert} +Device URL: {self.device_url} +Server PIN: {self.pin} +Verified? {verified} +Full certificate data:\n{cert_data}\nEND SUMMARY\n""") + + # Create client & query device capability, required before using client + client = IEEE2030_5_Client( + cafile=self.ca_cert, + server_hostname=self.server_address, + keyfile=self.key, + certfile=self.cert, + server_ssl_port=self.server_port, + debug=True + ) + client.device_capability() + + return client + + def new_uuid(self, client): + return client.new_uuid().replace("-", "") + + def build_mirror_usage_points(self, client): + mup_mrid = self.new_uuid(client) + mirror_readings = [] + for (i, reading) in enumerate(self.readings): + + mRID = self.new_uuid(client) + reading_type = self._reading_type_for_name(reading["type"], reading.get("power_of_ten_multiplier", 0)) + + mirror_readings.append( + m.MirrorMeterReading( + mRID=mRID, + lastUpdateTime=int(time.time()), + # Use the configured tag as canonical signal identity so + # the dashboard can render per-signal cards/charts. + description=reading["tag"] or reading["description"], + Reading=m.Reading(localID=reading["local_id"], value=0), + ReadingType=reading_type + ) + ) + + reading["mrid"] = mRID + self.readings[i] = reading + + mup = m.MirrorUsagePoint(mRID=mup_mrid, + deviceLFDI=self.lfdi, + MirrorMeterReading=mirror_readings) + status, mup_href = client.create_mirror_usage_point(mup) + assert status in (200, 201), f"MUP creation failed with status {status}" + + return (mup_mrid, mup_href) + + def build_batched_mirror_usage_point(self): + now_epoch = int(time.time()) + mirror_readings = [] + for reading in self.readings: + normalized_tag = self._normalize_tag(reading['tag']) + value = int(self.local_state.get(normalized_tag, 0)) + mirror_readings.append( + m.MirrorMeterReading( + lastUpdateTime=now_epoch, + description=reading['tag'] or reading['description'], + Reading=m.Reading(localID=reading['local_id'], value=value), + ReadingType=self._reading_type_for_name(reading['type'], reading.get('power_of_ten_multiplier', 0)), + ) + ) + + return m.MirrorUsagePoint( + deviceLFDI=self.lfdi, + postRate=self.polling_rate, + MirrorMeterReading=mirror_readings, + ) + + def connect_to_existing_mirror_usage_points(self, client): + pass + + def _resolve_device_href(self) -> str: + # Prefer the registration Location if it is valid. + if getattr(self, 'device_url', None): + try: + device = self.client.end_device_by_href(self.device_url) + if getattr(device, 'FunctionSetAssignmentsListLink', None): + return self.device_url + except Exception: + pass + + # Discover from EndDeviceList and match on SFDI when possible. + try: + end_device_list = self.client.end_devices() + end_devices = list(getattr(end_device_list, 'EndDevice', []) or []) + my_sfdi = str(getattr(self, 'sfdi', '')) + + for end_device in end_devices: + href = getattr(end_device, 'href', None) + if href and str(getattr(end_device, 'sFDI', '')) == my_sfdi: + return href + except Exception: + pass + + # Our EndDevice is not on the server (e.g. after a server restart that + # wiped in-memory state). Re-register so the server creates it again. + self.log('EndDevice not found on server — re-registering…') + try: + self.device_url = self.register_device( + self.cert, self.key, self.ca_cert, + self.server_address, self.server_port, + self.sfdi, + name=self.device_name, + ) + self.log(f'Re-registered EndDevice at {self.device_url}') + # Rebuild MUP so the server maps telemetry to the new EndDevice entry. + self.mup_mrid, self.mup_href = self.build_mirror_usage_points(self.client) + self.log(f'Rebuilt MUP: mrid={self.mup_mrid}, href={self.mup_href}') + return self.device_url + except Exception as exc: + self.log(f'Re-registration failed: {exc}') + + raise RuntimeError('Unable to resolve an EndDevice href for control polling') + + def listen_20305(self): + while self.running: + try: + now_epoch = int(time.time()) + if not self._resolved_device_href: + self._resolved_device_href = self._resolve_device_href() + + device = self.client.end_device_by_href(self._resolved_device_href) + + controls_with_primacy, default_control = self.client.der_controls_across_programs(device) + selected_control = self.client.select_active_control( + controls=None, + default_control=default_control, + current_time=now_epoch, + device_category_bitmap=self.device_category_bitmap_int, + controls_with_primacy=controls_with_primacy, + ) + + if selected_control is not None: + ctrl_mrid = getattr(selected_control, 'mRID', None) + reply_to = getattr(selected_control, 'replyTo', None) + rr = getattr(selected_control, 'responseRequired', b'\x00') + requires_start_ack = isinstance(rr, bytes) and len(rr) > 0 and (rr[-1] & 0x02) + if ( + ctrl_mrid and reply_to and requires_start_ack + and ctrl_mrid != self._acknowledged_control_mrid + ): + self.client.post_event_response(reply_to, ctrl_mrid, self.lfdi, status=2) + self._acknowledged_control_mrid = ctrl_mrid + + if self.control_setpoint_tags: + update_points = [] + for tag in self.control_setpoint_tags: + setpoint = self._extract_control_setpoint_w(selected_control, output_tag=tag) + if setpoint is None: + continue + update_points.append({ + 'tag': tag, + 'value': self._ramped_setpoint_value( + tag=tag, + selected_control=selected_control, + default_control=default_control, + target_value=setpoint, + now_epoch=now_epoch, + ), + 'ts': now_epoch, + }) + + if update_points: + runtime_update = envelope.new_update_envelope(self.name, {'updates': update_points}) + self.pusher.push('RUNTIME', runtime_update) + elif not self._logged_missing_setpoint_tags: + self.log('Skipping control output publish because selected control has no publishable setpoint value') + self._logged_missing_setpoint_tags = True + elif not self._logged_missing_setpoint_tags: + self.log('Skipping control output publish because no allowed setpoint tags are configured') + self._logged_missing_setpoint_tags = True + + telemetry = self.build_batched_mirror_usage_point() + status, mup_href = self.client.create_mirror_usage_point(telemetry) + if status == 403: + # Server rejected telemetry — our EndDevice registration was + # lost (e.g. server restart). Re-register on the next loop. + self.log("MUP rejected with 403 — EndDevice not enrolled; forcing re-registration.") + self._resolved_device_href = None + self.device_url = None + elif status in (200, 201) and mup_href: + self.mup_href = mup_href + except Exception as e: + # Reset resolved href on device-shape or missing-resource failures. + if 'FunctionSetAssignmentsListLink' in str(e) or '404' in str(e): + self._resolved_device_href = None + import traceback + self.log(f'2030.5 poll error: {e}') + self.log(f'Exception type: {type(e).__name__}') + self.log(traceback.format_exc()) + + time.sleep(self.polling_rate) + + # On update received from zmq + def listen_msgbus_updates(self, env: Envelope): + update = envelope.update_from_envelope(env) + if not update: + return + self._apply_points(update['updates']) + + # On status received from zmq + def listen_msgbus_status(self, env: Envelope): + status = envelope.status_from_envelope(env) + if not status: + return + self._apply_points(status['measurements']) + + def _apply_points(self, points) -> None: + for point in points: + normalized_tag = self._normalize_tag(point.get('tag')) + reading = self.readings_by_tag.get(normalized_tag) + if reading is None: + continue + try: + value = float(point.get('value', 0)) + except (TypeError, ValueError): + continue + multiplier = reading.get('power_of_ten_multiplier', 0) + self.local_state[normalized_tag] = int(round(value / (10 ** multiplier))) + + + def start(self): + self.subscriber.start('RUNTIME') + + self.client = self.initialize_client() + + self.mup_mrid, self.mup_href = self.build_mirror_usage_points(self.client) + self.log(f"mrid {self.mup_mrid}, mup {self.mup_href}") + + + self.running = True + self.poll_thread = threading.Thread(target=self.listen_20305, daemon=True) + self.poll_thread.start() + + def stop(self): + self.running = False + self.poll_thread.join(self.polling_rate) + self.subscriber.stop() + +def main(): + if len(sys.argv) < 2: + print('no config file provided') + sys.exit(1) + + config_path = Path(sys.argv[1]).resolve() + tree = ET.parse(config_path) + root = tree.getroot() + assert root.tag == 'ot-sim' + + mb = root.find('message-bus') + + if mb: + pub = mb.findtext('pub-endpoint') + pull = mb.findtext('pull-endpoint') + else: + pub = 'tcp://127.0.0.1:5678' + pull = 'tcp://127.0.0.1:1234' + + devices: typing.List[IEEE20305Client] = [] + + for client in root.findall('ieee20305-client'): + device = IEEE20305Client(pub, pull, client, config_dir=config_path.parent) + device.start() + devices.append(device) + + waiter = threading.Event() + + def handler(*_): + waiter.set() + + waiter.wait() + + for device in devices: + device.stop() diff --git a/src/python/otsim/ieee_2030_5/client_helper/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/__init__.py new file mode 100644 index 0000000..82cc13f --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/__init__.py @@ -0,0 +1,19 @@ +from .protocol_models import DerControlBase +from .protocol_models import DerControlEvent +from .protocol_models import DerProgramModel +from .protocol_models import DeviceIdentity +from .protocol_models import MirrorUsageSample +from .protocol_models import TimeWindow +from .protocol_models import create_peak_hours_sample_program +from .protocol_models import create_sample_mup + +__all__ = [ + "DerControlBase", + "DerControlEvent", + "DerProgramModel", + "DeviceIdentity", + "MirrorUsageSample", + "TimeWindow", + "create_peak_hours_sample_program", + "create_sample_mup", +] diff --git a/src/python/otsim/ieee_2030_5/client_helper/certs.py b/src/python/otsim/ieee_2030_5/client_helper/certs.py new file mode 100644 index 0000000..05abbd7 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/certs.py @@ -0,0 +1,398 @@ +import argparse +import hashlib +import logging +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Tuple +import shutil +import yaml +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +__all__ = ['TLSRepository'] + +from .types_ import Lfdi, PathStr +from .utils.tls_wrapper import OpensslWrapper, TLSWrap +from .utils.cryptography_wrapper import CryptographyWrapper + +_log = logging.getLogger(__name__) + +PRIVATE_EXTENTION = 'pem' +CERTIFICATE_EXTENSION = 'crt' + +PRIVATE_EXTENTION = os.environ.get('2030_5_PRIVATE_EXTENSION', PRIVATE_EXTENTION) +CERTIFICATE_EXTENSION = os.environ.get('2030_5_PUBLIC_EXTENSION', CERTIFICATE_EXTENSION) + +GLOB_PRIVATE = f'*.{PRIVATE_EXTENTION}' +GLOB_CERT = f'*.{CERTIFICATE_EXTENSION}' + +def lfdi_from_fingerprint(fingerprint: str) -> Lfdi: + fp = fingerprint.replace(":", "") + return Lfdi(fp[:40]) + + +def sfdi_from_lfdi(lfdi: Lfdi) -> int: + assert len(lfdi) == 40, "lfdi must be 160-bits (40 hex characters) long." + hex_str = str(int(lfdi[:9], 16)) + check_bit = 0 + full_sum = sum([int(x) for x in hex_str]) + while not (full_sum + check_bit) % 10 == 0: + check_bit += 1 + return int(hex_str + str(check_bit)) + + +class TLSRepository: + + def __init__(self, + repo_dir: PathStr, + openssl_cnffile_template: PathStr, + serverhost: str, + proxyhost: str = None, + clear=False, + **kwargs): + if isinstance(repo_dir, str): + repo_dir = Path(repo_dir).expanduser().resolve() + if isinstance(openssl_cnffile_template, str): + openssl_cnffile_template = Path(openssl_cnffile_template).expanduser().resolve() + if not openssl_cnffile_template.exists(): + raise ValueError(f"openssl_cnffile does not exist {openssl_cnffile_template}") + self._repo_dir = repo_dir + self._certs_dir = repo_dir.joinpath("certs") + self._private_dir = repo_dir.joinpath("private") + self._combined_dir = repo_dir.joinpath("combined") + self._openssl_cnf_file = self._repo_dir.joinpath(openssl_cnffile_template.name) + self._common_names = {serverhost: serverhost} + if proxyhost: + self._common_names[proxyhost] = proxyhost + self._client_common_name_set = set() + + if clear and self._repo_dir.exists(): + shutil.rmtree(self._repo_dir) + + if not self._repo_dir.exists() or not self._certs_dir.exists() or \ + not self._private_dir.exists() or not self._combined_dir.exists(): + self._certs_dir.mkdir(parents=True) + self._private_dir.mkdir(parents=True) + self._combined_dir.mkdir(parents=True) + + index_txt = self._repo_dir.joinpath("index.txt") + serial = self._repo_dir.joinpath("serial") + + if not index_txt.exists(): + index_txt.write_text("") + if not serial.exists(): + serial.write_text("01") + + self._current_pk: Dict[str, Path] = {} + self._current_certs: Dict[str, Path] = {} + # lfdi -> sfdi and sfdi -> lfdi for devices. + self._devices: Dict[str, str] = {} + + new_contents = openssl_cnffile_template.read_text().replace( + "dir = REPLACE_WITH_REPO_PATH", f"dir = {repo_dir}") + self._openssl_cnf_file.write_text(new_contents) + self._ca_key = self._private_dir / f"ca.{PRIVATE_EXTENTION}" + self._ca_cert = self._certs_dir / f"ca.{CERTIFICATE_EXTENSION}" + self._serverhost = serverhost + self._proxyhost = proxyhost + + self._tls: TLSWrap = OpensslWrapper(self._openssl_cnf_file) + # self._cert_paths: List[Path] = [] + # self._certificate_specs: Dict[str, Dict[str, str]] = {} + if not clear: + + # creating certs has something screwy so we are going + # to create the cert_paths based upon the private key + # files. + for f in self._private_dir.glob(GLOB_PRIVATE): + f = Path(f) + self._current_pk[f.stem] = f + + for f in self._certs_dir.glob(GLOB_CERT): + f = Path(f) + self._current_certs[f.stem] = f + + if not self._ca_key.exists() or not self._ca_cert.exists(): + self._tls.tls_create_private_key(self.ca_key_file) + self._tls.tls_create_ca_certificate("ca", self.ca_key_file, self.ca_cert_file) + self._current_pk["ca"] = self.ca_key_file + self._current_certs["ca"] = self.ca_cert_file + + if not self.server_cert_file.exists() or not self.server_key_file.exists(): + self._tls.tls_create_private_key(self.server_key_file) + self._tls.tls_create_signed_certificate(serverhost, self.ca_key_file, self.ca_cert_file, + self.server_key_file, self.server_cert_file, + as_server=True) + self._current_pk[serverhost] = self.server_key_file + self._current_certs[serverhost] = self.server_cert_file + + if proxyhost is not None and (not self.proxy_cert_file.exists() or \ + not self.proxy_key_file.exists()): + self._tls.tls_create_private_key(self.proxy_key_file) + self._tls.tls_create_signed_certificate(proxyhost, self.ca_key_file, self.ca_cert_file, + self.proxy_key_file, self.proxy_cert_file, + as_server=True) + self._current_pk[proxyhost] = self.proxy_key_file + self._current_certs[proxyhost] = self.proxy_cert_file + + generate_admin_cert = kwargs.pop('generate_admin_cert', False) + + if generate_admin_cert: + admin_key = self._private_dir / f"admin.{PRIVATE_EXTENTION}" + admin_cert = self._certs_dir / f"admin.{CERTIFICATE_EXTENSION}" + if not admin_key.exists(): + self._tls.tls_create_private_key(admin_key) + self.create_cert(admin_cert.stem) + self._current_pk["admin"] = admin_key + self._current_certs["admin"] = admin_cert + + + + for crt in self._current_certs: + if crt not in (serverhost, proxyhost, "ca", "admin"): + self._devices[crt] = (self.lfdi(crt), self.sfdi(crt)) + + assert len(self._current_pk) == len(self._current_certs) + + if len(kwargs) > 0: + raise ValueError(f"Not all kwargs used: {kwargs.keys()}") + + def __create_ca__(self): + self._tls.tls_create_private_key(self._ca_key) + self._tls.tls_create_ca_certificate("ca", self._ca_key, self._ca_cert) + self._tls.tls_create_pkcs23_pem_and_cert(self._ca_key, self._ca_cert, + self.__get_combined_file__("ca")) + self._current_certs["ca"] = self.ca_cert_file + self._current_pk["ca"] = self.ca_key_file + + def has_device(self, common_name: str) -> bool: + return common_name in self._devices + + def create_cert(self, common_name: str, as_server: bool = False): + + if not self.__get_key_file__(common_name).exists(): + self._tls.tls_create_private_key(self.__get_key_file__(common_name)) + self._current_pk[common_name] = self.__get_key_file__(common_name) + + self._tls.tls_create_signed_certificate(common_name, self._ca_key, self._ca_cert, + self.__get_key_file__(common_name), + self.__get_cert_file__(common_name), as_server) + self._current_certs[common_name] = self.__get_cert_file__(common_name) + + self._tls.tls_create_pkcs23_pem_and_cert(self.__get_key_file__(common_name), + self.__get_cert_file__(common_name), + self.__get_combined_file__(common_name)) + + # self._common_names[common_name] = common_name + # self._cert_paths.append(self.__get_cert_file__(common_name=common_name)) + # self._certificate_specs[common_name] = dict(common_name=common_name, + # lFDI=self.lfdi(common_name), + # path=self.__get_cert_file__(common_name).as_posix()) + + + def lfdi(self, device_id: str) -> Lfdi: + """ + Using the fingerprint of the certifcate return the left truncation of 160 bits with no check digit. + Example: + From: + 3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5-349E-2AD7-4567-2ED1-45EE-213A + Return: + 3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5 + as an integer. + """ + # 160 / 4 == 40 + fp = self.fingerprint(device_id, True) + return Lfdi(lfdi_from_fingerprint(fp)) + + def sfdi(self, device_id: str) -> int: + lfdi_ = self.lfdi(device_id) + return sfdi_from_lfdi(lfdi_) + + def fingerprint(self, device_id: str, without_colan: bool = True) -> str: + if os.environ.get('CLIENT_CERT_FROM_COMBINED_FILE'): + # _log.debug("Using hash from combined file.") + value = Path(self.__get_combined_file__(device_id)).read_text() + value = hashlib.sha256(value.encode('utf-8')).hexdigest() + else: + value = self._tls.tls_get_fingerprint_from_cert(self.__get_cert_file__(device_id)) + if without_colan: + value = value.replace(":", "") + if "=" in value: + value = value.split("=")[1] + assert isinstance(value, str) + return value + + def get_common_name(self, device_id: str) -> x509: + pem_data = Path(self.__get_cert_file__(device_id)).read_bytes() + cert = x509.load_pem_x509_certificate(pem_data, default_backend()) + return cert.subject.get_attributes_for_oid(x509.oid.NameOID.COMMON_NAME)[0].value + + def get_file_pair(self, device_id: str) -> Tuple[str, str]: + """ Get cert, key from the repository based on passed device_id""" + return (self.__get_cert_file__(device_id).as_posix(), + self.__get_key_file__(device_id).as_posix()) + + @property + def client_list(self) -> Dict[str, Dict[str, str]]: + # TODO: Use precalculated specs rather than this each time. + specs: Dict[str, Dict[str, str]] = {} + for d in self._private_dir.glob(GLOB_PRIVATE): + + paths = self.get_file_pair(d.stem) + + specs[d.stem] = {'common_name': d.stem, + 'path': ','.join(paths), + 'device': False} + + if ':' not in d.stem or 'admin' != d.stem: + specs[d.stem]['lFID'] = self.lfdi(d.stem) + specs[d.stem]['device'] = True + + return specs + + @property + def ca_key_file(self) -> Path: + return self._ca_key + + @property + def ca_cert_file(self) -> Path: + return self._ca_cert + + @property + def proxy_key_file(self) -> Path: + if not self._proxyhost: + raise ValueError("No proxy host available") + return self.__get_key_file__(self._proxyhost) + + @property + def proxy_cert_file(self) -> Path: + if not self._proxyhost: + raise ValueError("No proxy host available") + return self.__get_cert_file__(self._proxyhost) + + @property + def server_key_file(self) -> Path: + return self.__get_key_file__(self._serverhost) + + @property + def server_cert_file(self) -> Path: + return self.__get_cert_file__(self._serverhost) + + def find_device_id_from_sfdi(self, sfdi: int) -> Optional[str]: + """ + Searches the certificate paths for a device id that maps to the sfdi passed into the method. + Args: + sfdi: + + Returns: + + """ + device_id = None + _log.debug(f"Attempting to find sfid: {sfdi}") + for d in self._certs_dir.glob(GLOB_CERT): + try: + if sfdi == self.sfdi(d.stem): + device_id = d.stem + break + except FileNotFoundError: + pass + return device_id + + def __get_cert_file__(self, common_name: str) -> Path: + return self._certs_dir.joinpath(f"{common_name}.{CERTIFICATE_EXTENSION}") + + def __get_key_file__(self, common_name: str) -> Path: + return self._private_dir.joinpath(f"{common_name}.{PRIVATE_EXTENTION}") + + def __get_combined_file__(self, common_name: str) -> Path: + return self._combined_dir.joinpath(f"{common_name}-combined.{PRIVATE_EXTENTION}") + + +def _main(): + parser = argparse.ArgumentParser() + parser.add_argument("--dir", help="Directory of certificates determine lfdi and sfdi from.") + parser.add_argument("--file", help="File to determine lfdi and sfdi from.") + + opts = parser.parse_args() + + if opts.dir and opts.file: + sys.stderr.write("Only specify dir or file.\n") + sys.exit(1) + elif opts.dir is None and opts.file is None: + sys.stderr.write("Must specify either --dir or --file.\n") + sys.exit(1) + + target = opts.file + if opts.dir: + target = opts.dir + + target = Path(target) + if not target.exists(): + sys.stderr.write("Invalid file or directory refrence.\n") + sys.exit(1) + + if target.is_dir(): + targets = target.glob("*.crt") + else: + targets = [target] + + for t in targets: + fingerprint = OpensslWrapper.tls_get_fingerprint_from_cert(t) + lfdi = lfdi_from_fingerprint(fingerprint) + sfdi = sfdi_from_lfdi(lfdi) + sys.stdout.write(f"certificate: {t}\n") + sys.stdout.write(f"-" * 60 + "\n") + sys.stdout.write(f"fingerprint: {fingerprint}\n") + sys.stdout.write(f"lfdi: {lfdi.decode('ascii')}\n") + sys.stdout.write(f"sfdi: {sfdi}\n\n") + + +if __name__ == '__main__': + + logging.basicConfig(level=logging.DEBUG) + + # fingerprint = "3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5-349E-2AD7-4567-2ED1-45EE-213A".replace( + # "-", "") + # lfdi = lfdi_from_fingerprint(fingerprint) + # sfdi = sfdi_from_lfdi(lfdi) + # print(f"fingerprint: {fingerprint}") + # print(f"lfdi: {lfdi}") + # print(f"sfdi: {sfdi}") + + # fingerprint = "B5:65:B2:C4:D4:22:59:72:58:6E:4E:E2:B1:F2:98:D4:20:62:15:DB:53:49:AB:45:2F:D2:8F:BC:62:2C:28:1D".replace( + # ":", "") + # lfdi = lfdi_from_fingerprint(fingerprint) + # sfdi = sfdi_from_lfdi(lfdi) + # print(f"fingerprint: {fingerprint}") + # print(f"lfdi: {lfdi}") + # print(f"sfdi: {sfdi}") + + # + # tlsrepo = TLSRepository(repo_dir="~/tls", + # openssl_cnffile_template="../openssl.cnf", + # clear=False, + # serverhost="gridappsd_dev_2004:8443") + # fingerprint = tlsrepo.fingerprint("dev1") + # # fingerprint = "3F4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5-349E-2AD7-4567-2ED1-45EE-213B".replace("-", "") + # print(len(fingerprint)) + # print("my lfdi: ", fingerprint[:40]) + # tlsrepo.sfdi_from_lfdi(fingerprint[:40].encode("ascii")) + # # Each char is 4 bits so 9*4 == 36 + # print("left 36 bits: ", fingerprint[:9]) + # print("to int from fingerprint", int(fingerprint[:9], 16)) + # interum = str(int(fingerprint[:9], 16)) + # print(int(interum[-2:])) + # add_value = 1 + # while not (int(interum[-2:]) + add_value) % 10 == 0: + # add_value += 1 + # + # print(add_value) + # interum = interum + str(add_value) + # print(f"sfdi = ", interum) + # + # print(f" our sfdi: {tlsrepo.sfdi('dev1')}") + # + # _log.debug(f"fingerprint: {tlsrepo.fingerprint('dev1', False)}") + # + # _log.debug(f"dev1 lfdi: {tlsrepo.lfdi('dev1')}, sfdi: {tlsrepo.sfdi('dev1')}") diff --git a/src/python/otsim/ieee_2030_5/client_helper/client/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/client/__init__.py new file mode 100644 index 0000000..30fc072 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/client/__init__.py @@ -0,0 +1,5 @@ +from .client import IEEE2030_5_Client + +__all__ = [ + 'IEEE2030_5_Client' +] diff --git a/src/python/otsim/ieee_2030_5/client_helper/client/client.py b/src/python/otsim/ieee_2030_5/client_helper/client/client.py new file mode 100644 index 0000000..966618a --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/client/client.py @@ -0,0 +1,655 @@ +from __future__ import annotations + +import atexit +import http +import logging +import ssl +import threading +import time +import xml.dom.minidom +from http.client import HTTPSConnection +from os import PathLike +from pathlib import Path +from threading import Timer +from types import SimpleNamespace +from typing import Dict, List, Optional, Tuple, Any + +import werkzeug.middleware.lint +import xsdata + +from .. import utils +from ..utils import tls_wrapper as tls + +_log = logging.getLogger(__name__) +_log_req_resp = logging.getLogger(__name__ + ".request") + + +class IEEE2030_5_Client: + clients: set[IEEE2030_5_Client] = set() + + # noinspection PyUnresolvedReferences + def __init__(self, + cafile: PathLike, + server_hostname: str, + keyfile: PathLike, + certfile: PathLike, + server_ssl_port: Optional[int] = 443, + debug: bool = True): + + cafile = cafile if isinstance(cafile, PathLike) else Path(cafile) + keyfile = keyfile if isinstance(keyfile, PathLike) else Path(keyfile) + certfile = certfile if isinstance(certfile, PathLike) else Path(certfile) + + self._key = keyfile + self._cert = certfile + self._ca = cafile + self._server_hostname = server_hostname + self._server_ssl_port = server_ssl_port + + assert cafile.exists(), f"cafile doesn't exist ({cafile})" + assert keyfile.exists(), f"keyfile doesn't exist ({keyfile})" + assert certfile.exists(), f"certfile doesn't exist ({certfile})" + + self._using_client_cert = True + self._ssl_context = self._build_ssl_context(include_client_cert=True) + self._http_conn = self._new_http_conn(include_client_cert=True) + self._device_cap: Optional[Any] = None + self._mup: Optional[Any] = None + self._upt: Optional[Any] = None + self._edev: Optional[Any] = None + self._end_devices: Optional[Any] = None + self._fsa_list: Optional[Any] = None + self._debug = debug + self._dcap_poll_rate: int = 0 + self._dcap_timer: Optional[Timer] = None + self._disconnect: bool = False + self._tls = tls.OpensslWrapper + self._conn_lock = threading.Lock() + + IEEE2030_5_Client.clients.add(self) + + @property + def http_conn(self) -> HTTPSConnection: + if self._http_conn.sock is None: + self._http_conn.connect() + return self._http_conn + + def _build_ssl_context(self, include_client_cert: bool) -> ssl.SSLContext: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_REQUIRED + ssl_context.load_verify_locations(cafile=self._ca) + if include_client_cert: + ssl_context.load_cert_chain(certfile=self._cert, keyfile=self._key) + return ssl_context + + def _new_http_conn(self, include_client_cert: bool) -> HTTPSConnection: + self._using_client_cert = include_client_cert + self._ssl_context = self._build_ssl_context(include_client_cert=include_client_cert) + return HTTPSConnection(host=self._server_hostname, + port=self._server_ssl_port, + context=self._ssl_context) + + def register_end_device(self) -> str: + lfid = utils.get_lfdi_from_cert(self._cert) + sfid = utils.get_sfdi_from_lfdi(lfid) + if not self._device_cap: + self.device_capability() + response = self.__post__(self._device_cap.EndDeviceListLink.href, + data=f'{sfid}') + if response.status in (200, 201): + return response.headers.get("Location") + raise werkzeug.exceptions.Forbidden() + + def get(self, href): + return self.__get_request__(href) + + def is_end_device_registered(self, end_device: Any, pin: int) -> bool: + reg = self.registration(end_device) + return reg.pIN == pin + + def new_uuid(self, url: str = "/uuid") -> str: + res = self.__get_request__(url) + return res + + def end_devices(self) -> Any: + if not self._device_cap: + self.device_capability() + + self._end_devices = self.__get_request__(self._device_cap.EndDeviceListLink.href) + return self._end_devices + + def end_device(self, index: Optional[int] = 0) -> Any: + if not self._end_devices: + self.end_devices() + + return self._end_devices.EndDevice[index] + + def end_device_by_href(self, href: str) -> Any: + """Fetch a specific EndDevice resource by its canonical href. + + Preferred over end_device(index) in long-running loops — the index + is a position in a cached list and can drift if the list changes. + """ + return self.__get_request__(href) + + def function_set_assignment_for_device(self, device: Any, + fsa_index: int = 0) -> Any: + """Navigate directly from a device object to a FunctionSetAssignments item.""" + fsa_list = self.__get_request__(device.FunctionSetAssignmentsListLink.href) + return fsa_list.FunctionSetAssignments[fsa_index] + + def der_program_list_for_device(self, device: Any, + fsa_index: int = 0) -> Any: + fsa = self.function_set_assignment_for_device(device, fsa_index) + return self.__get_request__(fsa.DERProgramListLink.href) + + def der_control_list_for_device(self, device: Any, + fsa_index: int = 0, + derp_index: int = 0) -> Any: + derp_list = self.der_program_list_for_device(device, fsa_index) + derp = derp_list.DERProgram[derp_index] + link = getattr(derp, "DERControlListLink", None) + if link is None or getattr(link, "href", None) is None: + return None + return self.__get_request__(link.href) + + def default_der_control_for_device(self, device: Any, + fsa_index: int = 0, + derp_index: int = 0) -> Any: + derp_list = self.der_program_list_for_device(device, fsa_index) + derp = derp_list.DERProgram[derp_index] + link = getattr(derp, "DefaultDERControlLink", None) + if link is None or getattr(link, "href", None) is None: + return None + return self.__get_request__(link.href) + + def self_device(self) -> Any: + if not self._device_cap: + self.device_capability() + + return self.__get_request__(self._device_cap.SelfDeviceLink.href) + + def function_set_assignment_list(self, + edev_index: Optional[int] = 0 + ) -> Any: + fsa_list = self.__get_request__( + self.end_device(edev_index).FunctionSetAssignmentsListLink.href) + return fsa_list + + def function_set_assignment(self, + edev_index: Optional[int] = 0, + fsa_index: Optional[int] = 0) -> Any: + fsa_list = self.function_set_assignment_list(edev_index) + return fsa_list.FunctionSetAssignments[fsa_index] + + def der_list(self, edev_index: Optional[int] = 0) -> Any: + der_list = self.__get_request__(self.end_device(edev_index).DERListLink.href) + return der_list + + def poll_timer(self, fn, args): + if not self._disconnect: + _log.debug(threading.currentThread().name) + fn(args) + threading.currentThread().join() + + def device_capability(self, url: str = "/dcap") -> Any: + self._device_cap: Any = self.__get_request__(url) + if self._device_cap.pollRate is not None: + self._dcap_poll_rate = self._device_cap.pollRate + else: + self._dcap_poll_rate = 600 + + _log.debug(f"devcap id {id(self._device_cap)}") + _log.debug(threading.currentThread().name) + _log.debug(f"DCAP: Poll rate: {self._dcap_poll_rate}") + # self._dcap_timer = Timer(self._dcap_poll_rate, self.poll_timer, (self.device_capability, url)) + # self._dcap_timer.start() + return self._device_cap + + def time(self) -> Any: + timexml = self.__get_request__(self._device_cap.TimeLink.href) + return timexml + + def der_program_list(self, + edev_index: Optional[int] = 0, + fsa_index: Optional[int] = 0) -> Any: + fsa = self.function_set_assignment(edev_index, fsa_index) + derp_list = self.__get_request__(fsa.DERProgramListLink.href) + return derp_list + + def der_program(self, + edev_index: Optional[int] = 0, + fsa_index: Optional[int] = 0, + derp_index: Optional[int] = 0) -> Any: + derp_list = self.der_program_list(edev_index, fsa_index) + return derp_list.DERProgram[derp_index] + + def der_control_list(self, + edev_index: Optional[int] = 0, + fsa_index: Optional[int] = 0, + derp_index: Optional[int] = 0) -> Any: + der_program = self.der_program(edev_index, fsa_index, derp_index) + control_list_link = getattr(der_program, "DERControlListLink", None) + if control_list_link is None or getattr(control_list_link, "href", None) is None: + return None + return self.__get_request__(control_list_link.href) + + def default_der_control(self, + edev_index: Optional[int] = 0, + fsa_index: Optional[int] = 0, + derp_index: Optional[int] = 0) -> Any: + der_program = self.der_program(edev_index, fsa_index, derp_index) + default_control_link = getattr(der_program, "DefaultDERControlLink", None) + if default_control_link is None or getattr(default_control_link, "href", None) is None: + return None + return self.__get_request__(default_control_link.href) + + @staticmethod + def select_active_control(controls: Any, + default_control: Any, + current_time: Optional[int] = None, + device_category_bitmap: Optional[int] = None, + controls_with_primacy: Optional[List[Tuple[Any, int]]] = None, + ) -> Any: + """Select the highest-priority currently-active DERControl. + + Preferred call: pass controls_with_primacy=[(ctrl, program_primacy), ...] + from der_controls_across_programs() so that multi-program primacy ordering + is respected (§11.9.1: lower primacy value = higher priority). + + Legacy call: pass controls= (DERControlList or list) for single-program use. + primacy defaults to 0 in that case. + """ + # EventStatus codes that mean the event is NOT active (§11.9.1) + _INACTIVE = {0, 2, 3, 4} # Scheduled=0, Cancelled=2, CancelledWithRand=3, Superseded=4 + + if current_time is None: + current_time = int(time.time()) + + # Build a uniform list of (control, primacy) pairs + if controls_with_primacy is not None: + pairs = controls_with_primacy + else: + if controls is None: + control_items: List[Any] = [] + elif isinstance(controls, list): + control_items = controls + else: + control_items = getattr(controls, "DERControl", []) or [] + pairs = [(c, 0) for c in control_items] + + active: List[Tuple[Any, int]] = [] + for control, primacy in pairs: + status = getattr(getattr(control, "EventStatus", None), "currentStatus", None) + if status in _INACTIVE: + continue + + # deviceCategory bitmap filtering: skip events whose category mask doesn't + # include this device's category bits. + event_cat = getattr(control, "deviceCategory", None) + if event_cat is not None and device_category_bitmap is not None: + event_bits = (int.from_bytes(event_cat, 'big') + if isinstance(event_cat, bytes) else int(event_cat)) + if not (event_bits & device_category_bitmap): + continue + + interval = getattr(control, "interval", None) + if interval is None: + # No time window — active if server explicitly marked it Active (status=1) + if status == 1: + active.append((control, primacy)) + continue + + start = getattr(interval, "start", None) + duration = getattr(interval, "duration", None) + if start is None or duration is None: + continue + if start <= current_time < (start + duration): + active.append((control, primacy)) + + if active: + # Sort: primacy ascending (lower = more authoritative), + # then creationTime descending (newer wins within same primacy). + active.sort(key=lambda pair: ( + pair[1], + -(getattr(pair[0], "creationTime", 0) or 0), + )) + return active[0][0] + + return default_control + + def mirror_usage_point_list(self) -> Any: + self._mup = self.__get_request__(self._device_cap.MirrorUsagePointListLink.href) + return self._mup + + def usage_point_list(self) -> Any: + self._upt = self.__get_request__(self._device_cap.UsagePointListLink.href) + return self._upt + + def registration(self, end_device: Any) -> Any: + reg = self.__get_request__(end_device.RegistrationLink.href) + return reg + + def timelink(self): + if self._device_cap is None: + raise ValueError("Request device capability first") + return self.__get_request__(url=self._device_cap.TimeLink.href) + + def disconnect(self): + self._disconnect = True + if self._dcap_timer: + self._dcap_timer.cancel() + IEEE2030_5_Client.clients.remove(self) + + def request(self, endpoint: str, body: dict = None, method: str = "GET", headers: dict = None): + + if method.upper() == 'GET': + return self.__get_request__(endpoint, body, headers=headers) + + if method.upper() == 'POST': + print("Doing post") + return self.__post__(endpoint, body, headers=headers) + + def create_mirror_usage_point(self, mirror_usage_point: Any) -> Tuple[int, str]: + """Post a MirrorUsagePoint to the server. + + The server matches on deviceLFDI: if this device already has a MUP it + returns 200 + existing Location; if not it creates one and returns 201. + Either way the caller gets the canonical MUP href in the return value. + No client-side href caching needed — the server handles idempotency. + """ + data = utils.dataclass_to_xml(mirror_usage_point) + resp = self.__post__(self._device_cap.MirrorUsagePointListLink.href, data=data) + location = resp.headers.get('Location') or '' + return resp.status, location + + def post_event_response(self, reply_to: str, subject_mrid: bytes, + lfdi_hex: str, status: int = 1) -> None: + """POST a DERControlResponse to acknowledge a DERControl event (§11.9.1). + + status: 1=Received, 2=Started execution, 3=Completed. + Only sent when the control carries a replyTo URL (requires subscriptions + to be implemented server-side). Failures are swallowed so the control + loop is not interrupted. + """ + from .. import models as m + response_obj = m.DERControlResponse( + createdDateTime=int(time.time()), + endDeviceLFDI=bytes.fromhex(lfdi_hex), + status=status, + subject=subject_mrid, + ) + try: + data = utils.dataclass_to_xml(response_obj) + self.__post__(reply_to, data=data, + headers={'Content-Type': 'application/sep+xml'}) + except Exception as exc: + _log.warning("DERControlResponse POST to %s failed: %s", reply_to, exc) + + def put_der_capability(self, device: Any, capability: Any) -> int: + """PUT DERCapability to the device's DER resource (§10.4).""" + der_list_link = getattr(device, 'DERListLink', None) + if not der_list_link: + _log.warning("EndDevice has no DERListLink — skipping DERCapability PUT") + return 0 + try: + der_list = self.__get_request__(der_list_link.href) + ders = getattr(der_list, 'DER', []) or [] + if not ders: + _log.warning("DERList is empty — skipping DERCapability PUT") + return 0 + cap_link = getattr(ders[0], 'DERCapabilityLink', None) + if not cap_link: + _log.warning("DER has no DERCapabilityLink — skipping PUT") + return 0 + resp = self.__put__(cap_link.href, data=utils.dataclass_to_xml(capability)) + return resp.status + except Exception as exc: + _log.warning("DERCapability PUT failed: %s", exc) + return 0 + + def put_der_settings(self, device: Any, settings: Any) -> int: + """PUT DERSettings to the device's DER resource (§10.4).""" + der_list_link = getattr(device, 'DERListLink', None) + if not der_list_link: + return 0 + try: + der_list = self.__get_request__(der_list_link.href) + ders = getattr(der_list, 'DER', []) or [] + if not ders: + return 0 + settings_link = getattr(ders[0], 'DERSettingsLink', None) + if not settings_link: + return 0 + resp = self.__put__(settings_link.href, data=utils.dataclass_to_xml(settings)) + return resp.status + except Exception as exc: + _log.warning("DERSettings PUT failed: %s", exc) + return 0 + + def put_der_availability(self, device: Any, availability: Any) -> int: + """PUT DERAvailability to the device's DER resource (§10.4).""" + der_list_link = getattr(device, 'DERListLink', None) + if not der_list_link: + return 0 + try: + der_list = self.__get_request__(der_list_link.href) + ders = getattr(der_list, 'DER', []) or [] + if not ders: + return 0 + avail_link = getattr(ders[0], 'DERAvailabilityLink', None) + if not avail_link: + return 0 + resp = self.__put__(avail_link.href, data=utils.dataclass_to_xml(availability)) + return resp.status + except Exception as exc: + _log.warning("DERAvailability PUT failed: %s", exc) + return 0 + + def der_controls_across_programs(self, device: Any, + fsa_index: int = 0 + ) -> Tuple[List[Tuple[Any, int]], Optional[Any]]: + """Return all timed DERControls across every DERProgram, with primacy. + + Returns (controls_with_primacy, best_default_control) where: + - controls_with_primacy: list of (DERControl, program.primacy) from all programs + - best_default_control: DefaultDERControl from the highest-priority program + (lowest primacy value), None if none exist + + Use with select_active_control(controls_with_primacy=...) for correct + multi-program, primacy-aware event selection per §11.9.1. + """ + timed: List[Tuple[Any, int]] = [] + best_default = None + best_primacy = float('inf') + try: + # Some callers may pass href strings or stale 404 payloads here. + # Normalize to a real EndDevice object before traversing FSA links. + if isinstance(device, str): + href_candidate = device if device.startswith('/edev_') else '/edev_0' + resolved = self.__get_request__(href_candidate) + if not isinstance(resolved, str): + device = resolved + if isinstance(device, str) or getattr(device, 'FunctionSetAssignmentsListLink', None) is None: + end_devices = self.end_devices() + for ed in (getattr(end_devices, 'EndDevice', []) or []): + if getattr(ed, 'FunctionSetAssignmentsListLink', None) is not None: + device = ed + break + + fsa = self.function_set_assignment_for_device(device, fsa_index) + derp_list = self.__get_request__(fsa.DERProgramListLink.href) + for program in (getattr(derp_list, 'DERProgram', []) or []): + primacy = int(getattr(program, 'primacy', 0) or 0) + + ctrl_link = getattr(program, 'ActiveDERControlListLink', None) + if ctrl_link and getattr(ctrl_link, 'href', None): + ctrl_list = self.__get_request__(ctrl_link.href) + for ctrl in (getattr(ctrl_list, 'DERControl', []) or []): + timed.append((ctrl, primacy)) + + if primacy < best_primacy: + default_link = getattr(program, 'DefaultDERControlLink', None) + if default_link and getattr(default_link, 'href', None): + best_default = self.__get_request__(default_link.href) + best_primacy = primacy + except Exception as exc: + _log.warning("der_controls_across_programs failed: %s", exc) + return timed, best_default + + def create_mirror_meter_reading(self, mirror_usage_point_href: str, + mirror_meter_reading: Any) -> Tuple[int, str]: + data = utils.dataclass_to_xml(mirror_meter_reading) + resp = self.__post__(mirror_usage_point_href, data=data) + return resp.status, resp.headers['Location'] + + def post(self, url: str, data: Any, headers: Optional[Dict[str, str]] = None): + response = self.__post__(url, data, headers=headers) + + def __get_request__(self, url: str, body=None, headers: dict = None): + if headers is None: + headers = {"Connection": "keep-alive", "keep-alive": "timeout=30, max=1000"} + + if self._debug: + print(f"----> GET REQUEST") + print(f"url: {url} body: {body}") + with self._conn_lock: + try: + self.http_conn.request(method="GET", url=url, body=body, headers=headers) + except http.client.CannotSendRequest: + self._http_conn.close() + _log.debug("Reconnecting to server for GET") + self.http_conn.request(method="GET", url=url, body=body, headers=headers) + response = self._http_conn.getresponse() + response_data = response.read().decode("utf-8") + + response_obj = None + try: + response_obj = utils.xml_to_dataclass(response_data) + resp_xml = xml.dom.minidom.parseString(response_data) + if resp_xml and self._debug: + print(f"<---- GET RESPONSE") + print(f"{response_data}") # toprettyxml()}") + + except xsdata.exceptions.ParserError as ex: + if self._debug: + print(f"<---- GET RESPONSE") + print(f"{response_data}") + response_obj = response_data + + return response_obj + + def __close__(self): + self._http_conn.close() + self._ssl_context = None + self._http_conn = None + + def put(self, url: str, data: Any, headers: Optional[Dict[str, str]] = None): + response = self.__put__(url, data, headers=headers) + + def __put__(self, url: str, data: Any, headers: Optional[Dict[str, str]] = None): + if not headers: + headers = {'Content-Type': 'text/xml'} + + if self._debug: + _log_req_resp.debug(f"----> PUT REQUEST\nurl: {url}\nbody: {data}") + + with self._conn_lock: + try: + self.http_conn.request(method="PUT", headers=headers, url=url, body=data) + except http.client.CannotSendRequest: + self.http_conn.close() + _log.debug("Reconnecting to server") + self.http_conn.request(method="PUT", headers=headers, url=url, body=data) + + response = self._http_conn.getresponse() + body = response.read().decode("utf-8") + return SimpleNamespace(status=response.status, headers=response.headers, body=body) + + def __post__(self, url: str, data=None, headers: Optional[Dict[str, str]] = None): + if not headers: + headers = {'Content-Type': 'text/xml'} + + if self._debug: + _log_req_resp.debug(f"----> POST REQUEST\nurl: {url}\nbody: {data}") + + with self._conn_lock: + try: + self.http_conn.request(method="POST", headers=headers, url=url, body=data) + except http.client.CannotSendRequest: + self.http_conn.close() + _log.debug("Reconnecting to server for POST") + self.http_conn.request(method="POST", headers=headers, url=url, body=data) + response = self._http_conn.getresponse() + response_data = response.read().decode("utf-8") + if response_data and self._debug: + _log_req_resp.debug(f"<---- POST RESPONSE\n{response_data}") + + return SimpleNamespace(status=response.status, headers=response.headers, body=response_data) + + +# noinspection PyTypeChecker +def __release_clients__(): + for x in IEEE2030_5_Client.clients: + x.__close__() + IEEE2030_5_Client.clients = None + + +atexit.register(__release_clients__) + +# +# ssl_context = ssl.create_default_context(cafile=str(SERVER_CA_CERT)) +# +# +# con = HTTPSConnection("me.com", 8000, +# key_file=str(KEY_FILE), +# cert_file=str(CERT_FILE), +# context=ssl_context) +# con.request("GET", "/dcap") +# print(con.getresponse().read()) +# con.close() + +if __name__ == '__main__': + SERVER_CA_CERT = Path("~/tls/certs/ca.pem").expanduser().resolve() + KEY_FILE = Path("~/tls/private/dev1.pem").expanduser().resolve() + CERT_FILE = Path("~/tls/certs/dev1.pem").expanduser().resolve() + + headers = {'Connection': 'Keep-Alive', 'Keep-Alive': "max=1000,timeout=30"} + + h = IEEE2030_5_Client(cafile=SERVER_CA_CERT, + server_hostname="127.0.0.1", + server_ssl_port=8443, + keyfile=KEY_FILE, + certfile=CERT_FILE, + debug=True) + # h2 = IEEE2030_5_Client(cafile=SERVER_CA_CERT, server_hostname="me.com", ssl_port=8000, + # keyfile=KEY_FILE, certfile=KEY_FILE) + dcap = h.device_capability() + end_devices = h.end_devices() + + if not end_devices.all > 0: + print("registering end device.") + ed_href = h.register_end_device() + my_ed = h.end_devices() + my_fsa = h.function_set_assignment() + my_program = h.der_program() + + # ed = h.end_devices()[0] + # resp = h.request("/dcap", headers=headers) + # print(resp) + # resp = h.request("/dcap", headers=headers) + # print(resp) + #dcap = h.device_capability() + # get device list + #dev_list = h.request(dcap.EndDeviceListLink.href).EndDevice + + #ed = h.request(dev_list[0].href) + #print(ed) + # + # print(dcap.mirror_usage_point_list_link) + # # print(h.request(dcap.mirror_usage_point_list_link.href)) + # print(h.request("/dcap", method="post")) + + # tl = h.timelink() + #print(IEEE2030_5_Client.clients) diff --git a/src/python/otsim/ieee_2030_5/client_helper/hrefs.py b/src/python/otsim/ieee_2030_5/client_helper/hrefs.py new file mode 100644 index 0000000..dad1c0e --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/hrefs.py @@ -0,0 +1,950 @@ +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from functools import lru_cache +from typing import Any, List, NamedTuple, Optional, Union + +from . import models as m + +EDEV = "edev" +DCAP = "dcap" +UTP = "upt" +MUP = "mup" +DRP = "drp" +SDEV = "sdev" +MSG = "msg" +DER = "der" +CURVE = "dc" +RSPS = "rsps" +LOG = "log" +DERC = "derc" +DDERC = "dderc" +DERCA = "derca" +DERCURVE = "dc" +FSA = "fsa" +TIME = "tm" + +DER_PROGRAM = "derp" +# DER Available +DER_AVAILABILITY = "dera" +# DER Status +DER_STATUS = "ders" +DER_CONTROL_ACTIVE = DERCA +DER_CAPABILITY = "dercap" +# Settings +DER_SETTINGS = "derg" +END_DEVICE_REGISTRATION = "rg" +END_DEVICE_STATUS = "dstat" +END_DEVICE_FSA = FSA +END_DEVICE_POWER_STATUS = "ps" +END_DEVICE_LOG_EVENT_LIST = "lel" +END_DEVICE_INFORMATION = "di" + +DEFAULT_TIME_ROOT = f"/{TIME}" +DEFAULT_DCAP_ROOT = f"/{DCAP}" +DEFAULT_EDEV_ROOT = f"/{EDEV}" +DEFAULT_UPT_ROOT = f"/{UTP}" +DEFAULT_MUP_ROOT = f"/{MUP}" +DEFAULT_DRP_ROOT = f"/{DRP}" +DEFAULT_SELF_ROOT = f"/{SDEV}" +DEFAULT_MESSAGE_ROOT = f"/{MSG}" +DEFAULT_DER_ROOT = f"/{DER}" +DEFAULT_CURVE_ROOT = f"/{CURVE}" +DEFAULT_RSPS_ROOT = f"/{RSPS}" +DEFAULT_LOG_EVENT_ROOT = f"/{LOG}" +DEFAULT_FSA_ROOT = f"/{FSA}" +DEFAULT_DERP_ROOT = f"/{DER_PROGRAM}" +DEFAULT_DDERC_ROOT = f"/{DDERC}" + +SEP = "_" +MATCH_REG = "[a-zA-Z0-9_]*" + +# Used as a sentinal value when we only want the href of the root +NO_INDEX = -1 + + +class HrefParser: + + def __init__(self, href: str): + self.href = href + self._split = href.split(SEP) + + def has_index(self) -> bool: + """This function returns true if there is an index on the primary type. + + Ex: /edev_12_dstat has an index of 12 so this will return true + Ex: /edev has no index so this will return false + """ + return len(self._split) > 1 + + def count(self) -> int: + return len(self._split) + + def join(self, how_many: int) -> str: + return SEP.join([str(x) for x in self._split[:how_many]]) + + def startswith(self, value: str) -> bool: + return self.href.startswith(value) + + def at(self, index: int) -> Union[str, int, None]: + try: + intvalue = int(self._split[index]) + return intvalue + except ValueError: + return self._split[index] + except IndexError: + return None + + +class HrefEventParser(HrefParser): + + @property + def program_index(self) -> int: + return int(self.at(1)) + + @property + def event_index(self) -> int: + return int(self._split[-1]) + + @property + def events_href(self) -> str: + return SEP.join(self._split[:-1]) + + +class EndDeviceHref: + + def __init__(self, index: int = None, edev_href: str = None): + if index is None and edev_href is None: + raise ValueError("Must have either index or edev_href specified") + + if index is not None and edev_href is not None: + raise ValueError("Cannot have both index and edev_href specified") + + self.index = index + if edev_href is not None: + self.index = int(edev_href.split(SEP)[1]) + + self._root = SEP.join([DEFAULT_EDEV_ROOT, str(self.index)]) + + @staticmethod + def parse(href: str) -> EndDeviceHref: + index = int(href.split(SEP)[1]) + return EndDeviceHref(index) + + def __str__(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index)]) + + @property + def configuration(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), "cfg"]) + + @property + def der_list(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), DER]) + + @property + def device_information(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_INFORMATION]) + + @property + def device_status(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_STATUS]) + + @property + def power_status(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_POWER_STATUS]) + + @property + def registration(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_REGISTRATION]) + + @property + def function_set_assignments(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_FSA]) + + @property + def log_event_list(self) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(self.index), END_DEVICE_LOG_EVENT_LIST]) + + def fill_hrefs(self, enddevice: Any) -> Any: + """Populate canonical link objects on an EndDevice resource.""" + enddevice.href = self._root + enddevice.RegistrationLink = m.RegistrationLink(href=self.registration) + enddevice.DERListLink = m.DERListLink(href=self.der_list, all=0) + enddevice.FunctionSetAssignmentsListLink = m.FunctionSetAssignmentsListLink( + href=self.function_set_assignments, + all=0, + ) + enddevice.LogEventListLink = m.LogEventListLink(href=self.log_event_list, all=0) + enddevice.DeviceInformationLink = m.DeviceInformationLink(href=self.device_information) + enddevice.DeviceStatusLink = m.DeviceStatusLink(href=self.device_status) + enddevice.PowerStatusLink = m.PowerStatusLink(href=self.power_status) + return enddevice + + +# class DeviceCapabilityHref: + +# def __init__(self, index: int = None, enddevice: EndDeviceHref = None): +# if index is None and enddevice is None: +# raise ValueError(f"index and enddevice cannot both be None") +# elif index is not None and enddevice is not None: +# if enddevice.index != index: +# raise ValueError(f"index and enddevice.index must match if both are specified") + +# if enddevice is not None: +# index = enddevice.index + +# self.index = index + +# def __str__(self) -> str: +# return SEP.join([DEFAULT_DCAP_ROOT, str(self.index)]) + + +class DERSubType(Enum): + Capability = DER_CAPABILITY + Settings = DER_SETTINGS + Status = DER_STATUS + Availability = DER_AVAILABILITY + CurrentProgram = DER_PROGRAM + None_Available = NO_INDEX + + @classmethod + def parse(cls, value: str) -> DERSubType: + if value == END_DEVICE_STATUS: + return cls.Status + return cls(value) + + +class FSASubType(Enum): + DERProgram = "derp" + + +class DERProgramSubType(Enum): + NoLink = 0 + ActiveDERControlListLink = 1 + DefaultDERControlLink = 2 + DERControlListLink = 3 + DERCurveListLink = 4 + DERControlReplyTo = 5 + DERControl = 6 + + +class DERHref: + + def __init__(self, root: str) -> None: + """Constructs a DERHref. + + The root should be a single instance not a list. The properties + on this object will be the href of the link to the resourse. + """ + self.root = root + + @property + def der_availability(self) -> str: + return SEP.join([self.root, DER_AVAILABILITY]) + + @property + def der_status(self) -> str: + return SEP.join([self.root, DER_STATUS]) + + @property + def der_capability(self) -> str: + return SEP.join([self.root, DER_CAPABILITY]) + + @property + def der_settings(self) -> str: + return SEP.join([self.root, DER_SETTINGS]) + + @property + def der_current_program(self) -> str: + return SEP.join([self.root, DER_PROGRAM]) + + def fill_hrefs(self, der: Any) -> Any: + """Populate canonical link objects on a DER resource.""" + der.href = self.root + der.DERAvailabilityLink = m.DERAvailabilityLink(href=self.der_availability) + der.DERStatusLink = m.DERStatusLink(href=self.der_status) + der.DERCapabilityLink = m.DERCapabilityLink(href=self.der_capability) + der.DERSettingsLink = m.DERSettingsLink(href=self.der_settings) + der.CurrentDERProgramLink = m.CurrentDERProgramLink(href=self.der_current_program) + return der + + +class DeviceCapabilityHref: + + def __init__(self, end_device_index: str) -> None: + self._end_device_index = end_device_index + self.root = DEFAULT_DCAP_ROOT + #SEP.join([DEFAULT_EDEV_ROOT, self._end_device_index, DER, DER_CAPABILITY]) + #m.DeviceCapability + + @property + def enddevice_href(self) -> str: + return DEFAULT_EDEV_ROOT + + @property + def mirror_usage_point_href(self) -> str: + return DEFAULT_MUP_ROOT + + @property + def self_device_href(self) -> str: + return DEFAULT_SELF_ROOT + + @property + def time_href(self) -> str: + return DEFAULT_TIME_ROOT + + @property + def usage_point_href(self) -> str: + return DEFAULT_UPT_ROOT + + def fill_hrefs(self, dcap: Any) -> Any: + """Populate canonical discovery links on a DeviceCapability resource.""" + dcap.href = self.root + dcap.EndDeviceListLink = m.EndDeviceListLink(href=self.enddevice_href, all=0) + dcap.MirrorUsagePointListLink = m.MirrorUsagePointListLink( + href=self.mirror_usage_point_href, + all=0, + ) + dcap.SelfDeviceLink = m.SelfDeviceLink(href=self.self_device_href) + dcap.TimeLink = m.TimeLink(href=self.time_href) + dcap.UsagePointListLink = m.UsagePointListLink(href=self.usage_point_href, all=0) + return dcap + + +class DERProgramHref: + + def __init__(self, program_index: int) -> None: + self._root = SEP.join([DEFAULT_DERP_ROOT, str(program_index)]) + + @property + def active_control_href(self) -> str: + return SEP.join([self._root, DER_CONTROL_ACTIVE]) + + @property + def default_control_href(self) -> str: + return SEP.join([self._root, DDERC]) + + @property + def der_control_list_href(self) -> str: + return SEP.join([self._root, DERC]) + + @property + def der_curve_list_href(self) -> str: + return SEP.join([self._root, CURVE]) + + def fill_hrefs(self, program: Any) -> Any: + """Populate canonical link objects on a DERProgram resource.""" + program.href = self._root + program.ActiveDERControlListLink = m.ActiveDERControlListLink( + href=self.active_control_href, + all=0, + ) + program.DefaultDERControlLink = m.DefaultDERControlLink(href=self.default_control_href) + program.DERControlListLink = m.DERControlListLink(href=self.der_control_list_href, all=0) + program.DERCurveListLink = m.DERCurveListLink(href=self.der_curve_list_href, all=0) + return program + + +class DERProgramHrefOld(NamedTuple): + root: str + index: int + derp_subtype: DERProgramSubType = DERProgramSubType.NoLink + derp_subtype_index: int = NO_INDEX + + @staticmethod + def parse(href: str) -> DERProgramHrefOld: + parsed = href.split(SEP) + if len(parsed) == 1: + return DERProgramHrefOld(parsed[0], NO_INDEX) + elif len(parsed) == 2: + return DERProgramHrefOld(parsed[0], int(parsed[1])) + else: + mapped = dict( + derc=DERProgramSubType.DERControlListLink, + derca=DERProgramSubType.ActiveDERControlListLink, + dderc=DERProgramSubType.DefaultDERControlLink, + ) + if len(parsed) == 4: + return DERProgramHrefOld(parsed[0], int(parsed[1]), mapped[parsed[2]], + int(parsed[3])) + return DERProgramHrefOld(parsed[0], int(parsed[1]), mapped[parsed[2]]) + + +def der_program_parse(href: str) -> DERProgramHrefOld: + return DERProgramHrefOld.parse(href) + + +def der_program_href(index: int = NO_INDEX, + sub: DERProgramSubType = DERProgramSubType.NoLink, + subindex: int = NO_INDEX) -> str: + if index == NO_INDEX: + return DEFAULT_DERP_ROOT + + if sub == DERProgramSubType.NoLink: + return SEP.join([DEFAULT_DERP_ROOT, str(index)]) + + if sub == DERProgramSubType.ActiveDERControlListLink: + if subindex == NO_INDEX: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DER_CONTROL_ACTIVE]) + else: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DER_CONTROL_ACTIVE, str(subindex)]) + + if sub == DERProgramSubType.DefaultDERControlLink: + if subindex == NO_INDEX: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DDERC]) + else: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DDERC, str(subindex)]) + + if sub == DERProgramSubType.DERCurveListLink: + if subindex == NO_INDEX: + return SEP.join([DEFAULT_DERP_ROOT, str(index), CURVE]) + else: + return SEP.join([DEFAULT_DERP_ROOT, str(index), CURVE, str(subindex)]) + + if sub == DERProgramSubType.DERControlListLink: + if subindex == NO_INDEX: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DERC]) + else: + return SEP.join([DEFAULT_DERP_ROOT, str(index), DERC, str(subindex)]) + + if sub == DERProgramSubType.DERControlReplyTo: + return DEFAULT_RSPS_ROOT + + +@lru_cache() +def get_server_config_href() -> str: + return "/server/cfg" + + +@lru_cache() +def get_enddevice_list_href() -> str: + return DEFAULT_EDEV_ROOT + + +@lru_cache() +def curve_href(index: int = NO_INDEX) -> str: + if index == NO_INDEX: + return DEFAULT_CURVE_ROOT + + return SEP.join([DEFAULT_CURVE_ROOT, str(index)]) + + +@lru_cache() +def fsa_href(index: int = NO_INDEX, edev_index: int = NO_INDEX): + if index == NO_INDEX and edev_index == NO_INDEX: + return DEFAULT_FSA_ROOT + elif index != NO_INDEX and edev_index == NO_INDEX: + return SEP.join([DEFAULT_FSA_ROOT, str(index)]) + elif index == NO_INDEX and edev_index != NO_INDEX: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), FSA]) + else: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), FSA, str(index)]) + + +def derp_href(edev_index: int, fsa_index: int) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), FSA, str(fsa_index), DER_PROGRAM]) + + +def der_href(index: int = NO_INDEX, fsa_index: int = NO_INDEX, edev_index: int = NO_INDEX): + if index == NO_INDEX and fsa_index == NO_INDEX and edev_index == NO_INDEX: + return DEFAULT_DER_ROOT + elif index != NO_INDEX and fsa_index == NO_INDEX and edev_index == NO_INDEX: + return SEP.join([DEFAULT_DER_ROOT, str(index)]) + elif index == NO_INDEX and fsa_index != NO_INDEX and edev_index == NO_INDEX: + return SEP.join([DEFAULT_FSA_ROOT, str(fsa_index), DER_PROGRAM]) + elif edev_index != NO_INDEX and fsa_index == NO_INDEX and index == NO_INDEX: + return SEP.join([DEFAULT_EDEV_ROOT, int(edev_index), FSA]) + elif edev_index != NO_INDEX and fsa_index != NO_INDEX and index == NO_INDEX: + return SEP.join([DEFAULT_EDEV_ROOT, int(edev_index), FSA, int(fsa_index)]) + else: + raise ValueError(f"index={index}, fsa_index={fsa_index}, edev_index={edev_index}") + + +def edev_der_href(edev_index: int, der_index: int = NO_INDEX) -> str: + if der_index == NO_INDEX: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), DER]) + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), DER, str(der_index)]) + + +class EDevSubType(Enum): + None_Available = NO_INDEX + Registration = END_DEVICE_REGISTRATION + DeviceStatus = END_DEVICE_STATUS + PowerStatus = END_DEVICE_POWER_STATUS + FunctionSetAssignments = END_DEVICE_FSA + LogEventList = END_DEVICE_LOG_EVENT_LIST + DeviceInformation = END_DEVICE_INFORMATION + DER = DER + + +@dataclass +class EdevHref: + edev_index: int + edev_subtype: EDevSubType = EDevSubType.None_Available + edev_subtype_index: int = NO_INDEX + edev_der_subtype: DERSubType = DERSubType.None_Available + + def __str__(self) -> str: + value = "/edev" + if self.edev_index != NO_INDEX: + value = f"{value}{SEP}{self.edev_index}" + + if self.edev_subtype != EDevSubType.None_Available: + value = f"{value}{SEP}{self.edev_subtype.value}" + + if self.edev_subtype_index != NO_INDEX: + value = f"{value}{SEP}{self.edev_subtype_index}" + + if self.edev_der_subtype != DERSubType.None_Available: + value = f"{value}{SEP}{self.edev_der_subtype.value}" + + return value + + def parse(path: str) -> EdevHref: + split_pth = path.split(SEP) + + if split_pth[0] != EDEV and split_pth[0][1:] != EDEV: + raise ValueError(f"Must start with {EDEV}") + + if len(split_pth) == 1: + return EdevHref(NO_INDEX) + elif len(split_pth) == 2: + return EdevHref(int(split_pth[1])) + elif len(split_pth) == 3: + return EdevHref(int(split_pth[1]), edev_subtype=EDevSubType(split_pth[2])) + elif len(split_pth) == 4: + return EdevHref(int(split_pth[1]), + edev_subtype=EDevSubType(split_pth[2]), + edev_subtype_index=int(split_pth[3])) + elif len(split_pth) == 5: + return EdevHref(int(split_pth[1]), + edev_subtype=EDevSubType(split_pth[2]), + edev_subtype_index=int(split_pth[3]), + edev_der_subtype=DERSubType.parse(split_pth[4])) + else: + raise ValueError("Out of bounds parsing.") + + def __eq__(self, other: object) -> bool: + return other.edev_index == self.edev_index and other.edev_subtype == self.edev_subtype, \ + other.edev_subtype_index == self.edev_subtype_index and other.edev_der_subtype == self.edev_der_subtype + + +class FSAHref(NamedTuple): + fsa_index: NO_INDEX + fsa_sub: FSASubType = None + + +def fsa_parse(path: str) -> FSAHref: + split_pth = path.split(SEP) + + if len(split_pth) == 1: + return FSAHref(NO_INDEX) + elif len(split_pth) == 2: + return FSAHref(int(split_pth[1])) + elif len(split_pth) == 3: + return FSAHref(int(split_pth[1]), fsa_sub=split_pth[2]) + + raise ValueError("Invalid parsing path.") + + +def der_sub_href(edev_index: int, index: int = NO_INDEX, subtype: DERSubType = None): + if subtype is None and index == NO_INDEX: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), DER]) + elif subtype is None: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), DER, str(index)]) + else: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), DER, str(index), subtype.value]) + + +@lru_cache() +def mirror_usage_point_href(mirror_usage_point_index: int = NO_INDEX): + """Mirror Usage Point hrefs + + /mup + /mup/{mirror_usage_point_index} + + + """ + if mirror_usage_point_index == NO_INDEX: + ret = DEFAULT_MUP_ROOT + else: + ret = SEP.join([DEFAULT_MUP_ROOT, str(mirror_usage_point_index)]) + + return ret + + +class ParsedUsagePointHref: + + def __init__(self, href: str): + self._href = href + self._split = href.split(SEP) + + def last_list(self) -> str: + """Assuming the parsed href has a reference to an item, return the container href. + """ + if self._split[-1].isnumeric(): + return SEP.join(self._split[:-1]) + return self._href + + def has_usage_point_index(self) -> bool: + return self.usage_point_index is not None + + def has_extra(self) -> bool: + return self.has_meter_reading_list() or \ + self.has_reading_list() or \ + self.has_reading_set_list() or \ + self.has_reading_set_reading_list() + + def has_meter_reading_list(self) -> bool: + try: + if retval := self._split[2] == "mr": + ... + return retval + except IndexError: + return False + + def has_reading_type(self) -> bool: + try: + if retval := self._split[4] == "rt": + ... + return retval + except IndexError: + return False + + def has_reading_set_list(self) -> bool: + try: + if retval := self._split[4] == "rs": + ... + return retval + except IndexError: + return False + + def has_reading_set_reading_list(self) -> bool: + try: + if retval := self._split[6] == "r": + ... + return retval + except IndexError: + return False + + def has_reading_list(self) -> bool: + try: + if retval := self._split[4] == "r" or self._split[6] == "r": + ... + return retval + except IndexError: + return False + + @property + def usage_point_index(self) -> Optional[int]: + try: + return int(self._split[1]) + except IndexError: + pass + return None + + @property + def meter_reading_index(self) -> Optional[int]: + try: + return int(self._split[3]) + except IndexError: + pass + return None + + @property + def reading_set_index(self) -> Optional[int]: + try: + if self._split[4] == "rs": + return int(self._split[5]) + except IndexError: + pass + return None + + @property + def reading_set_reading_index(self) -> Optional[int]: + try: + if self._split[6] == "r": + return int(self._split[7]) + except IndexError: + pass + return None + + @property + def reading_index(self) -> Optional[int]: + try: + if self._split[4] == "r": + return int(self._split[5]) + elif self._split[6] == "r": + return int(self._split[7]) + except IndexError: + pass + + return None + + +class UsagePointHref: + + def __init__(self, href: str = None, root: str = '/upt'): + self._href = href + self._root = root + + def is_root(self) -> bool: + return self._href == self._root + + def value(self) -> str: + return self._root + + def usage_point(self, usage_point_index: int): + return SEP.join([self._root, str(usage_point_index)]) + + def meterreading_list(self, usage_point_index: int) -> str: + return SEP.join([self._root, str(usage_point_index), "mr"]) + + def meterreading(self, usage_point_index: int, meter_reading_index: int) -> str: + return SEP.join([self.meterreading_list(usage_point_index), str(meter_reading_index)]) + + def readingset_list(self, usage_point_index: int, meter_reading_index: int) -> str: + return SEP.join([self.meterreading(usage_point_index, meter_reading_index), "rs"]) + + def readingtype(self, usage_point_index: int, meter_reading_index: int) -> str: + return SEP.join([self.meterreading(usage_point_index, meter_reading_index), "rt"]) + + def readingset(self, usage_point_index: int, meter_reading_index: int, + reading_set_index: int) -> str: + return SEP.join( + [self.readingset_list(usage_point_index, meter_reading_index), + str(reading_set_index)]) + + def readingsetreading_list(self, usage_point_index: int, meter_reading_index: int, + reading_set_index: int): + return SEP.join( + [self.readingset(usage_point_index, meter_reading_index, reading_set_index), "r"]) + + def readingsetreading(self, usage_point_index: int, meter_reading_index: int, + reading_set_index: int, reading_index: int): + return SEP.join([ + self.readingsetreading_list(usage_point_index, meter_reading_index, reading_set_index), + str(reading_index) + ]) + + def reading_list(self, usage_point_index: int, meter_reading_index: int) -> str: + return SEP.join([self.meterreading(usage_point_index, meter_reading_index), "r"]) + + def reading(self, usage_point_index: int, meter_reading_index: int, reading_index: int) -> str: + return SEP.join( + [self.reading_list(usage_point_index, meter_reading_index), + str(reading_index)]) + + +@dataclass +class MirrorUsagePointHref: + mirror_usage_point_index: int = NO_INDEX + meter_reading_list_index: int = NO_INDEX + meter_reading_index: int = NO_INDEX + reading_set_index: int = NO_INDEX + reading_index: int = NO_INDEX + + @staticmethod + def parse(href: str) -> MirrorUsagePointHref: + items = href.split(SEP) + if len(items) == 1: + return MirrorUsagePointHref() + + if len(items) == 2: + return MirrorUsagePointHref(items[1]) + + +def usage_point_href(usage_point_index: int | str = NO_INDEX, + meter_reading_list: bool = False, + meter_reading_list_index: int = NO_INDEX, + meter_reading_index: int = NO_INDEX, + meter_reading_type: bool = False, + reading_set: bool = False, + reading_set_index: int = NO_INDEX, + reading_index: int = NO_INDEX): + """Usage point hrefs + + /upt + /upt/{usage_point_index} + /upt/{usage_point_index}/mr + /upt/{usage_point_index}/mr/{meter_reading_index} + /upt/{usage_point_index}/mr/{meter_reading_index}/rt + /upt/{usage_point_index}/mr/{meter_reading_index}/rs + /upt/{usage_point_index}/mr/{meter_reading_index}/rs/{reading_set_index} + /upt/{usage_point_index}/mr/{meter_reading_index}/rs/{reading_set_index}/r + /upt/{usage_point_index}/mr/{meter_reading_index}/rs/{reading_set_index}/r/{reading_index} + + + + """ + if isinstance(usage_point_index, str): + base_upt = usage_point_index + else: + base_upt = DEFAULT_UPT_ROOT + + if usage_point_index == NO_INDEX: + ret = base_upt + else: + if isinstance(usage_point_index, str): + arr = [base_upt] + else: + arr = [DEFAULT_UPT_ROOT, str(usage_point_index)] + + if meter_reading_list: + if meter_reading_list_index == NO_INDEX: + arr.extend(["mr"]) + else: + arr.extend(["mr", str(meter_reading_list_index)]) + + ret = SEP.join(arr) + return ret + + +def get_der_program_list(fsa_href: str) -> str: + return SEP.join([fsa_href, "der"]) + + +def get_dr_program_list(fsa_href: str) -> str: + return SEP.join([fsa_href, "dr"]) + + +def get_fsa_list_href(end_device_href: str) -> str: + return SEP.join([end_device_href, "fsa"]) + + +def get_response_set_href(): + return DEFAULT_RSPS_ROOT + + +@lru_cache() +def get_der_list_href(index: int) -> str: + if index == NO_INDEX: + ret = DEFAULT_DER_ROOT + else: + ret = SEP.join([DEFAULT_DER_ROOT, str(index)]) + return ret + + +@lru_cache() +def get_enddevice_href(edev_indx: int = NO_INDEX, subref: str = None) -> str: + if edev_indx == NO_INDEX: + ret = DEFAULT_EDEV_ROOT + elif subref: + ret = SEP.join([DEFAULT_EDEV_ROOT, f"{edev_indx}", f"{subref}"]) + else: + ret = SEP.join([DEFAULT_EDEV_ROOT, f"{edev_indx}"]) + return ret + + +@lru_cache() +def registration_href(edev_index: int) -> str: + return SEP.join([DEFAULT_EDEV_ROOT, str(edev_index), "rg"]) + + +@lru_cache() +def get_configuration_href(edev_index: int) -> str: + return get_enddevice_href(edev_index, "cfg") + + +@lru_cache() +def get_power_status_href(edev_index: int) -> str: + return get_enddevice_href(edev_index, "ps") + + +@lru_cache() +def get_device_status(edev_index: int) -> str: + return get_enddevice_href(edev_index, "ds") + + +@lru_cache() +def get_device_information(edev_index: int) -> str: + return get_enddevice_href(edev_index, "di") + + +@lru_cache() +def get_time_href() -> str: + # return f"{DEFAULT_DCAP_ROOT}{SEP}tm" + return f"/tm" + + +@lru_cache() +def get_log_list_href(edev_index: int) -> str: + return get_enddevice_href(edev_index, "lel") + + +@lru_cache() +def get_dcap_href() -> str: + return f"{DEFAULT_DCAP_ROOT}" + + +def get_dderc_href() -> str: + return SEP.join([DEFAULT_DER_ROOT, DDERC]) + + +def get_derc_default_href(derp_index: int) -> str: + return SEP.join([DEFAULT_DER_ROOT, DDERC, f"{derp_index}"]) + + +def get_derc_href(index: int) -> str: + """Return the DERControl href to the caller + + if NO_INDEX then don't include the index in the result. + """ + if index == NO_INDEX: + return SEP.join([DEFAULT_DER_ROOT, DERC]) + + return SEP.join([DEFAULT_DER_ROOT, DERC, f"{index}"]) + + +def get_program_href(index: int, subref: str = None): + """Return the DERProgram href to the caller + + Args: + index: if NO_INDEX then don't include the index in the result else use the index + subref: used to specify a subsection in the program. + """ + if index == NO_INDEX: + ref = f"{DEFAULT_DERP_ROOT}" + else: + if subref is not None: + ref = f"{DEFAULT_DERP_ROOT}{SEP}{index}{SEP}{subref}" + else: + ref = f"{DEFAULT_DERP_ROOT}{SEP}{index}" + return ref + + +sdev: str = DEFAULT_SELF_ROOT + +admin: str = "/admin" +uuid_gen: str = "/uuid" + + +def build_link(base_url: str, *suffix: Optional[str]): + result = base_url + if result.endswith("/"): + result = result[:-1] + + if suffix: + for p in suffix: + if p is not None: + if isinstance(p, str): + if p.startswith("/"): + result += f"{p}" + else: + result += f"/{p}" + else: + result += f"/{p}" + + return result + + +def extend_url(base_url: str, index: Optional[int] = None, suffix: Optional[str] = None): + result = base_url + if index is not None: + result += f"/{index}" + if suffix: + result += f"/{suffix}" + + return result diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/Config.py b/src/python/otsim/ieee_2030_5/client_helper/models/Config.py new file mode 100644 index 0000000..7e7f36e --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/Config.py @@ -0,0 +1,313 @@ +from __future__ import annotations +from dataclasses import dataclass, field +from typing import List, Optional + +__NAMESPACE__ = "http://pypi.org/project/xsdata" + + +@dataclass +class TypeName: + + class Meta: + name = "ClassName" + namespace = "http://pypi.org/project/xsdata" + + case: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + safePrefix: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class CompoundFields: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + defaultName: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + forceDefaultName: Optional[bool] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + value: Optional[bool] = field(default=None, metadata={ + "required": True, + }) + + +@dataclass +class ConstantName: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + case: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + safePrefix: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class FieldName: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + case: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + safePrefix: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class Format: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + repr: Optional[bool] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + eq: Optional[bool] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + order: Optional[bool] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + unsafeHash: Optional[bool] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + frozen: Optional[bool] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + slots: Optional[bool] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + kwOnly: Optional[bool] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + value: str = field(default="", metadata={ + "required": True, + }) + + +@dataclass +class ModuleName: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + case: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + safePrefix: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class PackageName: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + case: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + safePrefix: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class Substitution: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + type_value: Optional[str] = field(default=None, + metadata={ + "name": "type", + "type": "Attribute", + "required": True, + }) + search: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + replace: Optional[str] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class Conventions: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + ClassName: Optional[TypeName] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + FieldName: Optional[FieldName] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + ConstantName: Optional[ConstantName] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + ModuleName: Optional[ModuleName] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + PackageName: Optional[PackageName] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Output: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + maxLineLength: Optional[int] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + Package: Optional[str] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + Format: Optional[Format] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + Structure: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + DocstringStyle: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + FilterStrategy: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + RelativeImports: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + CompoundFields: Optional[CompoundFields] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + PostponedAnnotations: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + UnnestClasses: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + IgnorePatterns: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Substitutions: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + Substitution: List[Substitution] = field(default_factory=list, + metadata={ + "type": "Element", + "min_occurs": 1, + }) + + +@dataclass +class Config: + + class Meta: + namespace = "http://pypi.org/project/xsdata" + + version: Optional[float] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + Output: Optional[Output] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + Conventions: Optional[Conventions] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + Substitutions: Optional[Substitutions] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/models/__init__.py new file mode 100644 index 0000000..4f2a5a1 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/__init__.py @@ -0,0 +1,388 @@ +from .Config import ( + TypeName, + CompoundFields, + Config, + ConstantName, + Conventions, + FieldName, + Format, + ModuleName, + Output, + PackageName, + Substitution, + Substitutions, +) +from .derforecasts import (DERFlexibility, DERForecast, DERForecastLink, + ForecastNumericType, ForecastParameter, + ForecastParameterSet, ForecastParameterSetList) +from .enums import CurveType, DeviceCategoryType, PrimacyType +from .sep import ( + DER, IEEE_802_15_4, AbstractDevice, AccountBalance, AccountBalanceLink, AccountingUnit, + ActiveBillingPeriodListLink, ActiveCreditRegisterListLink, ActiveDERControlListLink, + ActiveEndDeviceControlListLink, ActiveFlowReservationListLink, ActivePower, + ActiveProjectionReadingListLink, ActiveSupplyInterruptionOverrideListLink, + ActiveTargetReadingListLink, ActiveTextMessageListLink, ActiveTimeTariffIntervalListLink, + AmpereHour, ApparentPower, ApplianceLoadReduction, AppliedTargetReduction, + AssociatedDERProgramListLink, AssociatedUsagePointLink, BillingMeterReadingBase, BillingPeriod, + BillingPeriodList, BillingPeriodListLink, BillingReading, BillingReadingList, + BillingReadingListLink, BillingReadingSet, BillingReadingSetList, BillingReadingSetListLink, + Charge, Condition, Configuration, ConfigurationLink, ConnectStatusType, + ConsumptionTariffInterval, ConsumptionTariffIntervalList, ConsumptionTariffIntervalListLink, + CreditRegister, CreditRegisterList, CreditRegisterListLink, CreditTypeChange, + CurrentDERProgramLink, CurrentRMS, CurveData, CustomerAccount, CustomerAccountLink, + CustomerAccountList, CustomerAccountListLink, CustomerAgreement, CustomerAgreementList, + CustomerAgreementListLink, DateTimeInterval, DefaultDERControl, DefaultDERControlLink, + DemandResponseProgram, DemandResponseProgramLink, DemandResponseProgramList, + DemandResponseProgramListLink, DERAvailability, DERAvailabilityLink, DERCapability, + DERCapabilityLink, DERControl, DERControlBase, DERControlList, DERControlListLink, + DERControlResponse, DERCurve, DERCurveLink, DERCurveList, DERCurveListLink, DERLink, DERList, + DERListLink, DERProgram, DERProgramLink, DERProgramList, DERProgramListLink, DERSettings, + DERSettingsLink, DERStatus, DERStatusLink, DeviceCapability, DeviceCapabilityLink, + DeviceInformation, DeviceInformationLink, DeviceStatus, DeviceStatusLink, DRLCCapabilities, + DrResponse, DutyCycle, EndDevice, EndDeviceControl, EndDeviceControlList, + EndDeviceControlListLink, EndDeviceLink, EndDeviceList, EndDeviceListLink, EnvironmentalCost, + Error, Event, EventStatus, File, FileLink, FileList, FileListLink, FileStatus, FileStatusLink, + FixedPointType, FixedVar, FlowReservationRequest, FlowReservationRequestList, + FlowReservationRequestListLink, FlowReservationResponse, FlowReservationResponseList, + FlowReservationResponseListLink, FlowReservationResponseResponse, FreqDroopType, + FunctionSetAssignments, FunctionSetAssignmentsBase, FunctionSetAssignmentsList, + FunctionSetAssignmentsListLink, GPSLocationType, HistoricalReading, HistoricalReadingList, + HistoricalReadingListLink, IdentifiedObject, InverterStatusType, IPAddr, IPAddrList, + IPAddrListLink, IPInterface, IPInterfaceList, IPInterfaceListLink, Link, List_type, ListLink, + LLInterface, LLInterfaceList, LLInterfaceListLink, LoadShedAvailability, + LoadShedAvailabilityList, LoadShedAvailabilityListLink, LocalControlModeStatusType, LogEvent, + LogEventList, LogEventListLink, ManufacturerStatusType, MessagingProgram, MessagingProgramList, + MessagingProgramListLink, MeterReading, MeterReadingBase, MeterReadingLink, MeterReadingList, + MeterReadingListLink, MirrorMeterReading, MirrorMeterReadingList, MirrorReadingSet, + MirrorUsagePoint, MirrorUsagePointList, MirrorUsagePointListLink, Neighbor, NeighborList, + NeighborListLink, Notification, NotificationList, NotificationListLink, Offset, + OperationalModeStatusType, PEVInfo, PowerConfiguration, PowerFactor, PowerFactorWithExcitation, + PowerStatus, PowerStatusLink, Prepayment, PrepaymentLink, PrepaymentList, PrepaymentListLink, + PrepayOperationStatus, PrepayOperationStatusLink, PriceResponse, PriceResponseCfg, + PriceResponseCfgList, PriceResponseCfgListLink, ProjectionReading, ProjectionReadingList, + ProjectionReadingListLink, RandomizableEvent, RateComponent, RateComponentLink, + RateComponentList, RateComponentListLink, ReactivePower, ReactiveSusceptance, Reading, + ReadingBase, ReadingLink, ReadingList, ReadingListLink, ReadingSet, ReadingSetBase, + ReadingSetList, ReadingSetListLink, ReadingType, ReadingTypeLink, RealEnergy, Registration, + RegistrationLink, RequestStatus, Resource, RespondableIdentifiedObject, RespondableResource, + RespondableSubscribableIdentifiedObject, Response, ResponseList, ResponseListLink, ResponseSet, + ResponseSetList, ResponseSetListLink, RPLInstance, RPLInstanceList, RPLInstanceListLink, + RPLSourceRoutes, RPLSourceRoutesList, RPLSourceRoutesListLink, SelfDevice, SelfDeviceLink, + ServiceChange, ServiceSupplier, ServiceSupplierLink, ServiceSupplierList, SetPoint, + SignedRealEnergy, StateOfChargeStatusType, StorageModeStatusType, SubscribableIdentifiedObject, + SubscribableList, SubscribableResource, Subscription, SubscriptionBase, SubscriptionList, + SubscriptionListLink, SupplyInterruptionOverride, SupplyInterruptionOverrideList, + SupplyInterruptionOverrideListLink, SupportedLocale, SupportedLocaleList, + SupportedLocaleListLink, TargetReading, TargetReadingList, TargetReadingListLink, + TargetReduction, TariffProfile, TariffProfileLink, TariffProfileList, TariffProfileListLink, + Temperature, TextMessage, TextMessageList, TextMessageListLink, TextResponse, Time, + TimeConfiguration, TimeLink, TimeTariffInterval, TimeTariffIntervalList, + TimeTariffIntervalListLink, UnitValueType, UnsignedFixedPointType, UsagePoint, UsagePointBase, + UsagePointLink, UsagePointList, UsagePointListLink, VoltageRMS, WattHour, loWPAN) + +__all__ = [ + "TypeName", + "CompoundFields", + "Config", + "ConstantName", + "Conventions", + "FieldName", + "Format", + "ModuleName", + "Output", + "PackageName", + "Substitution", + "Substitutions", + "DERFlexibility", + "DERForecast", + "DERForecastLink", + "ForecastNumericType", + "ForecastParameter", + "ForecastParameterSet", + "ForecastParameterSetList", + "AbstractDevice", + "AccountBalance", + "AccountBalanceLink", + "AccountingUnit", + "ActiveBillingPeriodListLink", + "ActiveCreditRegisterListLink", + "ActiveDERControlListLink", + "ActiveEndDeviceControlListLink", + "ActiveFlowReservationListLink", + "ActivePower", + "ActiveProjectionReadingListLink", + "ActiveSupplyInterruptionOverrideListLink", + "ActiveTargetReadingListLink", + "ActiveTextMessageListLink", + "ActiveTimeTariffIntervalListLink", + "AmpereHour", + "ApparentPower", + "ApplianceLoadReduction", + "AppliedTargetReduction", + "AssociatedDERProgramListLink", + "AssociatedUsagePointLink", + "BillingMeterReadingBase", + "BillingPeriod", + "BillingPeriodList", + "BillingPeriodListLink", + "BillingReading", + "BillingReadingList", + "BillingReadingListLink", + "BillingReadingSet", + "BillingReadingSetList", + "BillingReadingSetListLink", + "Charge", + "Condition", + "Configuration", + "ConfigurationLink", + "ConnectStatusType", + "ConsumptionTariffInterval", + "ConsumptionTariffIntervalList", + "ConsumptionTariffIntervalListLink", + "CreditRegister", + "CreditRegisterList", + "CreditRegisterListLink", + "CreditTypeChange", + "CurrentDERProgramLink", + "CurrentRMS", + "CurveData", + "CurveType", + "CustomerAccount", + "CustomerAccountLink", + "CustomerAccountList", + "CustomerAccountListLink", + "CustomerAgreement", + "CustomerAgreementList", + "CustomerAgreementListLink", + "DER", + "DERAvailability", + "DERAvailabilityLink", + "DERCapability", + "DERCapabilityLink", + "DERControl", + "DERControlBase", + "DERControlList", + "DERControlListLink", + "DERControlResponse", + "DERCurve", + "DERCurveLink", + "DERCurveList", + "DERCurveListLink", + "DERLink", + "DERList", + "DERListLink", + "DERProgram", + "DERProgramLink", + "DERProgramList", + "DERProgramListLink", + "DERSettings", + "DERSettingsLink", + "DERStatus", + "DERStatusLink", + "DRLCCapabilities", + "DateTimeInterval", + "DefaultDERControl", + "DefaultDERControlLink", + "DemandResponseProgram", + "DemandResponseProgramLink", + "DemandResponseProgramList", + "DemandResponseProgramListLink", + "DeviceCapability", + "DeviceCapabilityLink", + "DeviceCategoryType", + "DeviceInformation", + "DeviceInformationLink", + "DeviceStatus", + "DeviceStatusLink", + "DrResponse", + "DutyCycle", + "EndDevice", + "EndDeviceControl", + "EndDeviceControlList", + "EndDeviceControlListLink", + "EndDeviceLink", + "EndDeviceList", + "EndDeviceListLink", + "EnvironmentalCost", + "Error", + "Event", + "EventStatus", + "File", + "FileLink", + "FileList", + "FileListLink", + "FileStatus", + "FileStatusLink", + "FixedPointType", + "FixedVar", + "FlowReservationRequest", + "FlowReservationRequestList", + "FlowReservationRequestListLink", + "FlowReservationResponse", + "FlowReservationResponseList", + "FlowReservationResponseListLink", + "FlowReservationResponseResponse", + "FreqDroopType", + "FunctionSetAssignments", + "FunctionSetAssignmentsBase", + "FunctionSetAssignmentsList", + "FunctionSetAssignmentsListLink", + "GPSLocationType", + "HistoricalReading", + "HistoricalReadingList", + "HistoricalReadingListLink", + "IEEE_802_15_4", + "IPAddr", + "IPAddrList", + "IPAddrListLink", + "IPInterface", + "IPInterfaceList", + "IPInterfaceListLink", + "IdentifiedObject", + "InverterStatusType", + "LLInterface", + "LLInterfaceList", + "LLInterfaceListLink", + "Link", + "List_type", + "ListLink", + "LoadShedAvailability", + "LoadShedAvailabilityList", + "LoadShedAvailabilityListLink", + "LocalControlModeStatusType", + "LogEvent", + "LogEventList", + "LogEventListLink", + "ManufacturerStatusType", + "MessagingProgram", + "MessagingProgramList", + "MessagingProgramListLink", + "MeterReading", + "MeterReadingBase", + "MeterReadingLink", + "MeterReadingList", + "MeterReadingListLink", + "MirrorMeterReading", + "MirrorMeterReadingList", + "MirrorReadingSet", + "MirrorUsagePoint", + "MirrorUsagePointList", + "MirrorUsagePointListLink", + "Neighbor", + "NeighborList", + "NeighborListLink", + "Notification", + "NotificationList", + "NotificationListLink", + "Offset", + "OperationalModeStatusType", + "PEVInfo", + "PowerConfiguration", + "PowerFactor", + "PowerFactorWithExcitation", + "PowerStatus", + "PowerStatusLink", + "PrepayOperationStatus", + "PrepayOperationStatusLink", + "Prepayment", + "PrepaymentLink", + "PrepaymentList", + "PrepaymentListLink", + "PriceResponse", + "PriceResponseCfg", + "PriceResponseCfgList", + "PriceResponseCfgListLink", + "ProjectionReading", + "ProjectionReadingList", + "ProjectionReadingListLink", + "RPLInstance", + "RPLInstanceList", + "RPLInstanceListLink", + "RPLSourceRoutes", + "RPLSourceRoutesList", + "RPLSourceRoutesListLink", + "RandomizableEvent", + "RateComponent", + "RateComponentLink", + "RateComponentList", + "RateComponentListLink", + "ReactivePower", + "ReactiveSusceptance", + "Reading", + "ReadingBase", + "ReadingLink", + "ReadingList", + "ReadingListLink", + "ReadingSet", + "ReadingSetBase", + "ReadingSetList", + "ReadingSetListLink", + "ReadingType", + "ReadingTypeLink", + "RealEnergy", + "Registration", + "RegistrationLink", + "RequestStatus", + "Resource", + "RespondableIdentifiedObject", + "RespondableResource", + "RespondableSubscribableIdentifiedObject", + "Response", + "ResponseList", + "ResponseListLink", + "ResponseSet", + "ResponseSetList", + "ResponseSetListLink", + "SelfDevice", + "SelfDeviceLink", + "ServiceChange", + "ServiceSupplier", + "ServiceSupplierLink", + "ServiceSupplierList", + "SetPoint", + "SignedRealEnergy", + "StateOfChargeStatusType", + "StorageModeStatusType", + "SubscribableIdentifiedObject", + "SubscribableList", + "SubscribableResource", + "Subscription", + "SubscriptionBase", + "SubscriptionList", + "SubscriptionListLink", + "SupplyInterruptionOverride", + "SupplyInterruptionOverrideList", + "SupplyInterruptionOverrideListLink", + "SupportedLocale", + "SupportedLocaleList", + "SupportedLocaleListLink", + "TargetReading", + "TargetReadingList", + "TargetReadingListLink", + "TargetReduction", + "TariffProfile", + "TariffProfileLink", + "TariffProfileList", + "TariffProfileListLink", + "Temperature", + "TextMessage", + "TextMessageList", + "TextMessageListLink", + "TextResponse", + "Time", + "TimeConfiguration", + "TimeLink", + "TimeTariffInterval", + "TimeTariffIntervalList", + "TimeTariffIntervalListLink", + "UnitValueType", + "UnsignedFixedPointType", + "UsagePoint", + "UsagePointBase", + "UsagePointLink", + "UsagePointList", + "UsagePointListLink", + "VoltageRMS", + "WattHour", + "loWPAN", +] diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/constants.py b/src/python/otsim/ieee_2030_5/client_helper/models/constants.py new file mode 100644 index 0000000..b8e9bbf --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/constants.py @@ -0,0 +1,200 @@ +import enum +""" Metering +""" + + +class RtgNormalCategoryType(enum.IntEnum): + not_specified = 0 + category_a = 1 + category_b = 2 + + +class RtgAbnormalCategoryType(enum.IntEnum): + not_specified = 0 + category_I = 1 + category_II = 2 + category_III = 3 + + +class DataQualifierType(enum.IntEnum): + Not_applicable = 0 + Average = 2 + Maximum = 8 + Minimum = 9 + Normal = 12 + Standard_deviation_of_population = 29 + Standard_deviation_of_sample = 30 + + +class CommodityType(enum.IntEnum): + Not_applicable = 0 + Electricity_secondary_metered = 1 + Electricity_primary_metered = 2 + Air = 4 + NaturalGas = 7 + Propane = 8 + PotableWater = 9 + Steam = 10 + WasteWater = 11 + HeatingFluid = 12 + CoolingFluid = 13 + + +class FlowDirectionType(enum.IntEnum): + Not_applicable = 0 + Forward = 1 + Reverse = 19 + + +class UomType(enum.IntEnum): + Not_applicable = 0 + Amperes = 5 + Kelvin = 6 + Degrees_celsius = 23 + Voltage = 29 + Joule = 31 + Hz = 33 + W = 38 + M_cubed = 42 + VA = 61 + VAr = 63 + CosTheta = 65 + V_squared = 67 + A_squared = 69 + VAh = 71 + Wh = 72 + VArh = 73 + Ah = 106 + Ft_cubed = 119 + Ft_cubed_per_hour = 122 + M_cubed_per_hour = 125 + US_gallons = 128 + UG_gallons_per_hour = 129 + Imperial_gallons = 130 + Imperial_gallons_per_hour = 131 + BTU = 132 + BTU_per_hour = 133 + Liter = 134 + Liter_per_hour = 137 + PA_gauge = 140 + PA_absolute = 155 + Therm = 169 + + +class RoleFlagsType(enum.Flag): + IsMirror = 0 + IsPremiseAggregationPoint = 1 + IsPEV = 2 + IsDER = 4 + IsRevenueQuality = 8 + IsDC = 16 + IsSubmeter = 32 + + +class AccumlationBehaviourType(enum.IntEnum): + Not_applicable = 0 + Cumulative = 3 + DeltaData = 4 + Indicating = 6 + Summation = 9 + Instantaneous = 12 + + +class ServiceKind(enum.IntEnum): + Electricity = 0 + Gas = 1 + Water = 2 + Time = 3 + Pressure = 4 + Heat = 5 + Cooling = 6 + + +class QualityFlagsType(enum.Flag): + Valid = 0 + Manually_edited = 1 + estimated_using_reference_day = 2 + estimated_using_linear_interpolation = 4 + questionable = 8 + derived = 16 + projected = 32 + + +# p163 +class ConsumptionBlockType(enum.IntEnum): + Not_applicable = 0 + Block_1 = 1 + Block_2 = 2 + Block_3 = 3 + Block_4 = 4 + Block_5 = 5 + Block_6 = 6 + Block_7 = 7 + Block_8 = 8 + Block_9 = 9 + Block_10 = 10 + Block_11 = 11 + Block_12 = 12 + Block_13 = 13 + Block_14 = 14 + Block_15 = 15 + Block_16 = 16 + + +# p170 +class TOUType(enum.IntEnum): + Not_applicable = 0 + TOU_A = 1 + TOU_B = 2 + TOU_C = 3 + TOU_D = 4 + TOU_E = 5 + TOU_F = 6 + TOU_G = 7 + TOU_H = 8 + TOU_I = 9 + TOU_J = 10 + TOU_K = 11 + TOU_L = 12 + TOU_M = 13 + TOU_N = 14 + TOU_O = 15 + + +class KindType(enum.IntEnum): + Not_applicable = 0 + Currency = 3 + Demand = 8 + Energy = 12 + Power = 37 + + +class PhaseCode(enum.IntEnum): + Not_applicable = 0 + Phase_C = 32 # and S2 + Phase_CN = 33 # and S2N + Phase_CA = 40 + Phase_B = 64 + Phase_BN = 65 + Phase_BC = 66 + Phase_A = 128 # and S1 + Phase_AN = 129 # and S1N + Phase_AB = 132 + Phase_ABC = 224 + + +""" Subscription/Notification +""" + + +class ResponseRequiredType(enum.Flag): + enddevice_shall_indicate_that_message_was_received = 0 + enddevice_shall_indicate_specific_response = 1 + enduser_customer_response_is_required = 2 + + +class SubscribableType(enum.IntEnum): + resource_does_not_support_subscriptions = 0 + resource_supports_non_conditional_subscriptions = 1 + resource_supports_conditional_subscriptions = 2 + resource_supports_both_conditional_and_non_conditional_subscriptions = 3 diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/derforecasts.py b/src/python/otsim/ieee_2030_5/client_helper/models/derforecasts.py new file mode 100644 index 0000000..9b925ab --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/derforecasts.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional +from .sep import ( + DER, + IdentifiedObject, + Link, + Resource, + SubscribableList, +) + +__NAMESPACE__ = "epri:derforecast:ns" + + +@dataclass +class ForecastNumericType: + """ + Real number expressed as an integer and power-of-ten-multiplier. + + :ivar value: Value expressed as integer + :ivar multiplier: Multiplier for value. Multiply value by 10^this. + """ + class Meta: + namespace = "epri:derforecast:ns" + + value: Optional[int] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + multiplier: Optional[int] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + + +@dataclass +class DERForecastLink(Link): + """ + SHALL contain a Link to an instance of DERForecast. + """ + class Meta: + namespace = "epri:derforecast:ns" + + postRate: int = field( + default=900, + metadata={ + "type": "Attribute", + } + ) + + +@dataclass +class ForecastParameter: + """ + Object holding forecast for a single parameter. + + :ivar name: Name of the paramater + :ivar forecast: Forecast for the parameter named. + :ivar sigma: Standard deviation for the parameter named. + """ + class Meta: + namespace = "epri:derforecast:ns" + + name: Optional[int] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + forecast: Optional[ForecastNumericType] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + sigma: Optional[ForecastNumericType] = field( + default=None, + metadata={ + "type": "Element", + } + ) + + +@dataclass +class DERFlexibility(DER): + """ + Extends sep DER to include DERForecastLink. + """ + class Meta: + namespace = "epri:derforecast:ns" + + DERForecastLink: Optional[DERForecastLink] = field( + default=None, + metadata={ + "type": "Element", + } + ) + + +@dataclass +class ForecastParameterSet(Resource): + """ + A set of forecasts. + """ + class Meta: + namespace = "epri:derforecast:ns" + + ForecastParameter: List[ForecastParameter] = field( + default_factory=list, + metadata={ + "type": "Element", + } + ) + + +@dataclass +class ForecastParameterSetList(SubscribableList): + """ + A List element to hold ForecastParameterSet objects. + """ + class Meta: + namespace = "epri:derforecast:ns" + + ForecastParameterSet: List[ForecastParameterSet] = field( + default_factory=list, + metadata={ + "type": "Element", + } + ) + + +@dataclass +class DERForecast(IdentifiedObject): + """ + DER forecast information. + + :ivar startTime: The start time in epoch for this forecast. + :ivar interval: Forecast interval for the included + ForecastParameterSetList, in seconds. + :ivar ForecastParameterSetList: + """ + class Meta: + namespace = "epri:derforecast:ns" + + startTime: Optional[int] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + interval: Optional[int] = field( + default=None, + metadata={ + "type": "Element", + "required": True, + } + ) + ForecastParameterSetList: Optional[ForecastParameterSetList] = field( + default=None, + metadata={ + "type": "Element", + } + ) diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/enums.py b/src/python/otsim/ieee_2030_5/client_helper/models/enums.py new file mode 100644 index 0000000..e14d7c1 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/enums.py @@ -0,0 +1,129 @@ +from enum import IntEnum + + +class PrimacyType(IntEnum): + # Values possible for indication of "Primary" provider: + # 0: In home energy management system + # 1: Contracted premises service provider + # 2: Non-contractual service provider + # 3 - 64: Reserved + # 65 - 191: User-defined + #192 - 255: Reserved + InHomeManagementSystem = 0 + ContractedPremisesServiceProvider = 1 + NonContractualServiceProvider = 2 + + +class DERUnitRefType(IntEnum): + # 0 - N/A + # 1 - %setMaxW + # 2 - %setMaxVar + # 3 - %statVarAvail + # 4 - %setEffectiveV + # 5 - %setMaxChargeRateW + # 6 - %setMaxDischargeRateW + # 7 - %statWAvail + NA = 0 + setMaxW = 1 + setMaxVar = 2 + statVarAvail = 3 + setEffectiveV = 4 + setMaxChargeRateW = 5 + setMaxDischargeRateW = 6 + statWAvail = 7 + +class CurveType(IntEnum): + # 0 - opModFreqWatt (Frequency-Watt Curve Mode) + # 1 - opModHFRTMayTrip (High Frequency Ride Through, May Trip Mode) + # 2 - opModHFRTMustTrip (High Frequency Ride Through, Must Trip Mode) + # 3 - opModHVRTMayTrip (High Voltage Ride Through, May Trip Mode) + # 4 - opModHVRTMomentaryCessation (High Voltage Ride Through, Momentary Cessation + # Mode) + # 5 - opModHVRTMustTrip (High Voltage Ride Through, Must Trip Mode) + # 6 - opModLFRTMayTrip (Low Frequency Ride Through, May Trip Mode) + # 7 - opModLFRTMustTrip (Low Frequency Ride Through, Must Trip Mode) + # 8 - opModLVRTMayTrip (Low Voltage Ride Through, May Trip Mode) + # 9 - opModLVRTMomentaryCessation (Low Voltage Ride Through, Momentary Cessation + # Mode) + # 10 - opModLVRTMustTrip (Low Voltage Ride Through, Must Trip Mode) + # 11 - opModVoltVar (Volt-Var Mode) + # 12 - opModVoltWatt (Volt-Watt Mode) + # 13 - opModWattPF (Watt-PowerFactor Mode) + # 14 - opModWattVar (Watt-Var Mode) + opModFreqWatt = 0 + opModHFRTMayTrip = 1 + opModHFRTMustTrip = 2 + opModHVRTMayTrip = 3 + opModHVRTMomentaryCessation = 4 + opModHVRTMustTrip = 5 + opModLFRTMayTrip = 6 + opModLFRTMustTrip = 7 + opModLVRTMayTrip = 8 + opModLVRTMomentaryCessation = 9 + opModLVRTMustTrip = 10 + opModVoltVar = 11 + opModVoltWatt = 12 + opModWattPF = 13 + opModWattVar = 14 + + +class DeviceCategoryType(IntEnum): + """ + DeviceCategoryType defined from 20305-2018_IIEStandardforSmartEnergyProfileApplicationsProtocol.pdf Appendix + B.2.3.4 Types package + """ + # The Device category types defined. + # Bit positions SHALL be defined as follows: + PROGRAMMABLE_COMMUNICATING_THERMOSTAT = 0 + STRIP_HEATERS = 1 + BASEBOARD_HEATERS = 2 + WATER_HEATER = 3 + POOL_PUMP = 4 + SAUNA = 5 + HOT_TUB = 6 + SMART_APPLIANCE = 7 + IRRIGATION_PUMP = 8 + MANAGED_COMMERCIAL_AND_INDUSTRIAL_LOADS = 9 + SIMPLE_RESIDENTIAL_LOADS = 10 # On/Off loads + EXTERIOR_LIGHTING = 11 + INTERIOR_LIGHTING = 12 + LOAD_CONTROL_SWITCH = 13 + ENERGY_MANAGEMENT_SYSTEM = 14 + SMART_ENERGY_MODULE = 15 + ELECTRIC_VEHICLE = 16 + ELECTRIC_VEHICLE_SUPPLY_EQUIPMENT = 17 + VIRTUAL_OR_MIXED_DER = 18 + RECIPROCATING_ENGINE = 19 # Synchronous Machine + FUEL_CELL = 20 # Battery + PHOTOVOLTAIC_SYSTEM = 21 # Solar + COMBINED_HEAT_AND_POWER = 22 + COMBINED_PV_AND_STORAGE = 23 + OTHER_GENERATION_SYSTEMS = 24 + OTHER_STORAGE_SYSTEMS = 25 + + # Additional here for Aggregator + AGGREGATOR = 99 + OTHER_CLIENT = 100 + + +# 0 - Programmable Communicating Thermostat +# 1 - Strip Heaters +# 2 - Baseboard Heaters +# 3 - Water Heater +# 4 - Pool Pump +# 5 - Sauna +# 6 - Hot tub +# 7 - Smart Appliance +# 8 - Irrigation Pump +# 9 - Managed Commercial and Industrial Loads +# 10 - Simple Residential Loads +# 11 - Exterior Lighting +# 12 - Interior Lighting +# 13 - Electric Vehicle +# 14 - Generation Systems +# 15 - Load Control Switch +# 16 - Smart Inverter +# 17 - EVSE +# 18 - Residential Energy Storage Unit +# 19 - Energy Management System +# 20 - Smart Energy Module diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/output/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/models/output/__init__.py new file mode 100644 index 0000000..ca9df36 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/output/__init__.py @@ -0,0 +1,28 @@ +''' + Annotated CIMantic Graphs data profile init file for Profile + Generated by CIMTool http://cimtool.org +''' +from ieee_2030_5.models.output.output_models import ( + Identity, ACDCTerminal, Analog, AnalogControl, AnalogValue, BatteryState, BatteryUnit, Command, + ConductingEquipment, Discrete, DiscreteValue, GeneratingUnit, + IEEE1547AbnormalPerfomanceCategory, IEEE1547ControlSettings, IEEE1547Info, + IEEE1547IslandingCategory, IEEE1547NormalPerformanceCategory, IEEE1547Setting, + IEEE1547TripSettings, IOPoint, IdentifiedObject, Measurement, MeasurementValue, + MeasurementValueQuality, MeasurementValueSource, PhaseCode, PhotoVoltaicUnit, + PowerElectronicsConnection, PowerElectronicsConnectionPhase, PowerElectronicsUnit, + PowerSystemResource, RotatingMachine, SinglePhaseKind, SmartInverterMode, Terminal, + ReactivePower, Susceptance, Seconds, Voltage, ApparentPower, PerCent, RealEnergy, PU, + Frequency, ActivePower) + +__all__ = [ + Identity, ACDCTerminal, Analog, AnalogControl, AnalogValue, BatteryState, BatteryUnit, Command, + ConductingEquipment, Discrete, DiscreteValue, GeneratingUnit, + IEEE1547AbnormalPerfomanceCategory, IEEE1547ControlSettings, IEEE1547Info, + IEEE1547IslandingCategory, IEEE1547NormalPerformanceCategory, IEEE1547Setting, + IEEE1547TripSettings, IOPoint, IdentifiedObject, Measurement, MeasurementValue, + MeasurementValueQuality, MeasurementValueSource, PhaseCode, PhotoVoltaicUnit, + PowerElectronicsConnection, PowerElectronicsConnectionPhase, PowerElectronicsUnit, + PowerSystemResource, RotatingMachine, SinglePhaseKind, SmartInverterMode, Terminal, + ReactivePower, Susceptance, Seconds, Voltage, ApparentPower, PerCent, RealEnergy, PU, + Frequency, ActivePower +] diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/output/output_models.py b/src/python/otsim/ieee_2030_5/client_helper/models/output/output_models.py new file mode 100644 index 0000000..e784f0c --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/output/output_models.py @@ -0,0 +1,2076 @@ +from __future__ import annotations +from dataclasses import dataclass, field, is_dataclass +from typing import Optional +from enum import Enum +from uuid import UUID, uuid4 +from random import Random +import json +import logging + +_log = logging.getLogger(__name__) +''' + Annotated CIMantic Graphs data profile for Profile + Generated by CIMTool http://cimtool.org +''' + + +@dataclass +class Identity(): + ''' + This is the new root class from CIM 18 to provide common identification + for all classes needing identification and naming attributes. + IdentifiedObject is now a child class of Identity. + mRID is superseded by Identity.identifier, which is typed to be a UUID. + ''' + identifier: Optional[str | UUID] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '1', + 'maxOccurs': '1' + }) + + # Override python __repr__ method with JSON-LD representation + # This is needed to avoid infinite loops in object previews + def __repr__(self) -> str: + return json.dumps({ + '@id': f'{str(self.identifier)}', + '@type': f'{self.__class__.__name__}' + }) + + # Override python string for printing with JSON representation + def __str__(self) -> str: + # Create JSON-LD dump with repr and all attributes + dump = dict(json.loads(self.__repr__()) | self.__dict__) + attribute_list = list(self.__dataclass_fields__.keys()) + for attribute in attribute_list: + # Delete attributes from print that are empty + if dump[attribute] is None or dump[attribute] == []: + del dump[attribute] + # If a dataclass, replace with custom repr + elif is_dataclass(dump[attribute]): + dump[attribute] = json.loads(dump[attribute].__repr__()) + # If a list, convert elements to string + elif type(dump[attribute]) == list: + values = [] + for value in dump[attribute]: + # If a dataclass, replace with custom repr + if is_dataclass(dump[attribute]): + values.append(json.loads(value.__repr__())) + else: + values.append(str(value)) + elif type[dump[attribute]] != str: + # Reformat all attributes as string for JSON + dump[attribute] = str(dump[attribute]) + # Fix python ' vs JSON " + dump = str(dump).replace('\'', '\"') + # Add 4 spaces indentation + dump = json.dumps(json.loads(dump), indent=4) + return dump + + # Create UUID from inconsistent mRIDs + def uuid(self, mRID: str = None, name: str = None) -> UUID: + invalid_mrid = False + # If mRID is specified, try creating from UUID from mRID + if mRID is not None: + try: + self.identifier = UUID(mRID.strip('_').lower(), version=4) + except: + invalid_mrid = True + name = mRID + _log.warning(f'mRID {mRID} not a valid UUID, generating new UUID') + # Otherwise, build UUID using unique name as a seed + elif invalid_mrid or name is not None: + seedStr = f"{self.__class__.__name__}:{name}" + randomGenerator = Random(seedStr) + self.identifier = UUID(int=randomGenerator.getrandbits(128), version=4) + else: + self.identifier = uuid4() + if 'mRID' in self.__dataclass_fields__: + if mRID is not None: + self.mRID = mRID + else: + self.mRID = str(self.identifier) + + +@dataclass(repr=False) +class AnalogControl(Identity): + ''' + An analog control used for supervisory control. + ''' + + +@dataclass(repr=False) +class Command(Identity): + ''' + A Command is a discrete control used for supervisory control. + ''' + + +@dataclass(repr=False) +class IEEE1547ControlSettings(Identity): + ''' + ''' + constantPowerFactor: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + constantReactivePower: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceIntentionalDelay: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMaxFrequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMaxVoltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMinFrequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMinVoltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + frequencyDroopResponseTime: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + openLoopResponseTimeP: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + overFrequencyDeadband: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + overFrequencyDroop: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + timeConstantOpenLoop: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + timeConstantReferenceVoltage: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + underFrequencyDeadband: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + underFrequencyDroop: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarQ1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarQ2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarQ3: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarQ4: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarV1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarV2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarV3: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltVarV4: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltWattP1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltWattP2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltWattV1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + voltWattV2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarP1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarP2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarP3: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarP4: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarQ1: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarQ2: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarQ3: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + wattVarQ4: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + PowerElectronicsConnections: list[str | PowerElectronicsConnection] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsConnection.IEEE1547ControlSettings' + }) + ''' + ''' + RotatingMachines: list[str | RotatingMachine] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'RotatingMachine.IEEE1547ControlSettings' + }) + ''' + ''' + + +@dataclass(repr=False) +class IEEE1547Info(Identity): + ''' + ''' + manufacturer: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + model: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + overExcitedPF: Optional[float] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + serialNumber: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsDynamicReactiveCurrent: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsIEC61850: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsIEEE1815: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsIEEE20305: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsIslanding: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsSunSpecModBusEthernet: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsSunSpecModBusRS485: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsVoltWatt: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + supportsWattVar: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + underExcitedPF: Optional[float] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + version: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + abnormalPerformanceCategory: Optional[str | IEEE1547AbnormalPerfomanceCategory] = field( + default=None, metadata={ + 'type': 'Enumeration', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + islandingCategory: Optional[str | IEEE1547IslandingCategory] = field(default=None, + metadata={ + 'type': 'enum', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + maximumU: Optional[float | Voltage] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + minimumU: Optional[float | Voltage] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + normalPerformanceCategory: Optional[str | IEEE1547NormalPerformanceCategory] = field( + default=None, metadata={ + 'type': 'enum', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedPatUnityPF: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedPcharge: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedPoverExcited: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedPunderExcited: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedQabsorbed: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedQinjected: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedS: Optional[float | ApparentPower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedScharge: Optional[float | ApparentPower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + ratedU: Optional[float | Voltage] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + susceptanceCeaseToEnergize: Optional[float | Susceptance] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + PowerElectronicsConnections: list[str | PowerElectronicsConnection] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsConnection.IEEE1547Info' + }) + ''' + ''' + RotatingMachines: list[str | RotatingMachine] = field(default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': + 'RotatingMachine.IEEE1547Info' + }) + ''' + ''' + + +@dataclass(repr=False) +class IEEE1547Setting(Identity): + ''' + ''' + constantPowerFactor: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + constantReactivePower: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceIntentionalDelay: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMaxFrequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMaxVoltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMinFrequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + enterServiceMinVoltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + frequencyDroopResponseTime: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + islandClearingTime: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + openLoopResponseTimeP: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + overFrequencyDeadband: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + overFrequencyDroop: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + timeConstantOpenLoop: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + timeConstantReferenceVoltage: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + underFrequencyDeadband: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + underFrequencyDroop: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + PowerElectronicsConnections: list[str | PowerElectronicsConnection] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsConnection.IEEE1547Setting' + }) + ''' + ''' + RotatingMachines: list[str | RotatingMachine] = field(default_factory=list, + metadata={ + 'type': + 'Association', + 'minOccurs': + '0', + 'maxOccurs': + 'unbounded', + 'inverse': + 'RotatingMachine.IEEE1547Setting' + }) + ''' + ''' + + +@dataclass(repr=False) +class IEEE1547TripSettings(Identity): + ''' + ''' + OF1frequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OF1time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OF2frequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OF2time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OV1time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OV1voltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OV2time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + OV2voltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UF1frequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UF1time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UF2frequency: Optional[float | Frequency] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UF2time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UV1time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UV1voltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UV2time: Optional[float | Seconds] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + UV2voltage: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + PowerElectronicsConnections: list[str | PowerElectronicsConnection] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsConnection.IEEE1547TripSettings' + }) + ''' + ''' + RotatingMachines: list[str | RotatingMachine] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'RotatingMachine.IEEE1547TripSettings' + }) + ''' + ''' + + +@dataclass(repr=False) +class IdentifiedObject(Identity): + ''' + This is a root class to provide common identification for all classes needing + identification and naming attributes. + ''' + mRID: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Master resource identifier issued by a model authority. The mRID is unique + within an exchange context. Global uniqueness is easily achieved by using + a UUID, as specified in RFC 4122, for the mRID. The use of UUID is strongly + recommended. + For CIMXML data files in RDF syntax conforming to IEC 61970-552 Edition + 1, the mRID is mapped to rdf:ID or rdf:about attributes that identify CIM + object elements. + ''' + aliasName: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The aliasName is free text human readable name of the object alternative + to IdentifiedObject.name. It may be non unique and may not correlate to + a naming hierarchy. + The attribute aliasName is retained because of backwards compatibility + between CIM relases. It is however recommended to replace aliasName with + the Name class as aliasName is planned for retirement at a future time. + ''' + description: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The description is a free human readable text describing or naming the + object. It may be non unique and may not correlate to a naming hierarchy. + ''' + name: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The name is any free human readable and possibly non unique text naming + the object. + ''' + + +@dataclass(repr=False) +class ACDCTerminal(IdentifiedObject): + ''' + An electrical connection point (AC or DC) to a piece of conducting equipment. + Terminals are connected at physical connection points called connectivity + nodes. + ''' + connected: Optional[bool] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The connected status is related to a bus-branch model and the topological + node to terminal relation. True implies the terminal is connected to the + related topological node and false implies it is not. + In a bus-branch model, the connected status is used to tell if equipment + is disconnected without having to change the connectivity described by + the topological node to terminal relation. A valid case is that conducting + equipment can be connected in one end and open in the other. In particular + for an AC line segment, where the reactive line charging can be significant, + this is a relevant case. + ''' + Measurements: list[str | Measurement] = field(default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'Measurement.Terminal' + }) + ''' + Measurements associated with this terminal defining where the measurement + is placed in the network topology. It may be used, for instance, to capture + the sensor position, such as a voltage transformer (PT) at a busbar or + a current transformer (CT) at the bar between a breaker and an isolator. + ''' + + +@dataclass(repr=False) +class Terminal(ACDCTerminal): + ''' + An AC electrical connection point to a piece of conducting equipment. Terminals + are connected at physical connection points called connectivity nodes. + ''' + ConductingEquipment: Optional[str | ConductingEquipment] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'ConductingEquipment.Terminals' + }) + ''' + The conducting equipment of the terminal. Conducting equipment have terminals + that may be connected to other conducting equipment terminals via connectivity + nodes or topological nodes. + ''' + + +@dataclass(repr=False) +class IOPoint(IdentifiedObject): + ''' + The class describe a measurement or control value. The purpose is to enable + having attributes and associations common for measurement and control. + ''' + + +@dataclass(repr=False) +class MeasurementValue(IOPoint): + ''' + The current state for a measurement. A state value is an instance of a + measurement from a specific source. Measurements can be associated with + many state values, each representing a different source for the measurement. + ''' + timeStamp: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The time when the value was last updated + ''' + sensorAccuracy: Optional[float | PerCent] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The limit, expressed as a percentage of the sensor maximum, that errors + will not exceed when the sensor is used under reference conditions. + ''' + MeasurementValueQuality: Optional[str | MeasurementValueQuality] = field( + default=None, + metadata={ + 'type': 'Aggregate Of', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'MeasurementValueQuality.MeasurementValue' + }) + ''' + A MeasurementValue has a MeasurementValueQuality associated with it. + ''' + MeasurementValueSource: Optional[str | MeasurementValueSource] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'MeasurementValueSource.MeasurementValues' + }) + ''' + A reference to the type of source that updates the MeasurementValue, e.g. + SCADA, CCLink, manual, etc. User conventions for the names of sources are + contained in the introduction to IEC 61970-301. + ''' + + +@dataclass(repr=False) +class AnalogValue(MeasurementValue): + ''' + AnalogValue represents an analog MeasurementValue. + ''' + value: Optional[float] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The value to supervise. + ''' + Analog: Optional[str | Analog] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'Analog.AnalogValues' + }) + ''' + Measurement to which this value is connected. + ''' + AnalogControl: Optional[str | AnalogControl] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'AnalogControl.AnalogValue' + }) + ''' + The Control variable associated with the MeasurementValue. + ''' + + +@dataclass(repr=False) +class DiscreteValue(MeasurementValue): + ''' + DiscreteValue represents a discrete MeasurementValue. + ''' + value: Optional[int] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + The value to supervise. + ''' + Command: Optional[str | Command] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'Command.DiscreteValue' + }) + ''' + The Control variable associated with the MeasurementValue. + ''' + Discrete: Optional[str | Discrete] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'Discrete.DiscreteValues' + }) + ''' + Measurement to which this value is connected. + ''' + + +@dataclass(repr=False) +class Measurement(IdentifiedObject): + ''' + A Measurement represents any measured, calculated or non-measured non-calculated + quantity. Any piece of equipment may contain Measurements, e.g. a substation + may have temperature measurements and door open indications, a transformer + may have oil temperature and tank pressure measurements, a bay may contain + a number of power flow measurements and a Breaker may contain a switch + status measurement. + The PSR - Measurement association is intended to capture this use of Measurement + and is included in the naming hierarchy based on EquipmentContainer. The + naming hierarchy typically has Measurements as leafs, e.g. Substation-VoltageLevel-Bay-Switch-Measurement. + Some Measurements represent quantities related to a particular sensor location + in the network, e.g. a voltage transformer (PT) at a busbar or a current + transformer (CT) at the bar between a breaker and an isolator. The sensing + position is not captured in the PSR - Measurement association. Instead + it is captured by the Measurement - Terminal association that is used to + define the sensing location in the network topology. The location is defined + by the connection of the Terminal to ConductingEquipment. + If both a Terminal and PSR are associated, and the PSR is of type ConductingEquipment, + the associated Terminal should belong to that ConductingEquipment instance. + When the sensor location is needed both Measurement-PSR and Measurement-Terminal + are used. The Measurement-Terminal association is never used alone. + ''' + measurementType: Optional[str] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Specifies the type of measurement. For example, this specifies if the measurement + represents an indoor temperature, outdoor temperature, bus voltage, line + flow, etc. + When the measurementType is set to "Specialization", the type of Measurement + is defined in more detail by the specialized class which inherits from + Measurement. + ''' + phases: Optional[str | PhaseCode] = field(default=None, + metadata={ + 'type': 'Enumeration', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Indicates to which phases the measurement applies and avoids the need to + use 'measurementType' to also encode phase information (which would explode + the types). The phase information in Measurement, along with 'measurementType' + and 'phases' uniquely defines a Measurement for a device, based on normal + network phase. Their meaning will not change when the computed energizing + phasing is changed due to jumpers or other reasons. + If the attribute is missing three phases (ABC) shall be assumed. + ''' + PowerSystemResource: Optional[str | PowerSystemResource] = field( + default=None, + metadata={ + 'type': 'Of Aggregate', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'PowerSystemResource.Measurements' + }) + ''' + The power system resource that contains the measurement. + ''' + Terminal: Optional[str | ACDCTerminal] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'ACDCTerminal.Measurements' + }) + ''' + One or more measurements may be associated with a terminal in the network. + ''' + + +@dataclass(repr=False) +class Analog(Measurement): + ''' + Analog represents an analog Measurement. + ''' + normalValue: Optional[float] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Normal measurement value, e.g., used for percentage calculations. + ''' + + +@dataclass(repr=False) +class Discrete(Measurement): + ''' + Discrete represents a discrete Measurement, i.e. a Measurement representing + discrete values, e.g. a Breaker position. + ''' + normalValue: Optional[int] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Normal measurement value, e.g., used for percentage calculations. + ''' + + +@dataclass(repr=False) +class MeasurementValueSource(IdentifiedObject): + ''' + MeasurementValueSource describes the alternative sources updating a MeasurementValue. + User conventions for how to use the MeasurementValueSource attributes are + described in the introduction to IEC 61970-301. + ''' + + +@dataclass(repr=False) +class PowerSystemResource(IdentifiedObject): + ''' + A power system resource can be an item of equipment such as a switch, an + equipment container containing many individual items of equipment such + as a substation, or an organisational entity such as sub-control area. + Power system resources can have measurements associated. + ''' + Measurements: list[str | Measurement] = field(default_factory=list, + metadata={ + 'type': 'Aggregate Of', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'Measurement.PowerSystemResource' + }) + ''' + The measurements associated with this power system resource. + ''' + + +@dataclass(repr=False) +class ConductingEquipment(PowerSystemResource): + ''' + The parts of the AC power system that are designed to carry current or + that are conductively connected through terminals. + ''' + + +@dataclass(repr=False) +class PowerElectronicsConnection(ConductingEquipment): + ''' + A connection to the AC network for energy production or consumption that + uses power electronics rather than rotating machines. + ''' + inverterMode: Optional[str | SmartInverterMode] = field(default=None, + metadata={ + 'type': 'enum', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + ''' + maxIFault: Optional[float | PU] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Maximum fault current this device will contribute, in per-unit of rated + current, before the converter protection will trip or bypass. + ''' + maxQ: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Maximum reactive power limit. This is the maximum (nameplate) limit for + the unit. + ''' + minQ: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Minimum reactive power limit for the unit. This is the minimum (nameplate) + limit for the unit. + ''' + p: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Active power injection. Load sign convention is used, i.e. positive sign + means flow out from a node. + Starting value for a steady state solution. + ''' + q: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Reactive power injection. Load sign convention is used, i.e. positive sign + means flow out from a node. + Starting value for a steady state solution. + ''' + ratedS: Optional[float | ApparentPower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Nameplate apparent power rating for the unit. + The attribute shall have a positive value. + ''' + ratedU: Optional[float | Voltage] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Rated voltage (nameplate data, Ur in IEC 60909-0). It is primarily used + for short circuit data exchange according to IEC 60909. + ''' + IEEE1547ControlSettings: Optional[str | IEEE1547ControlSettings] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547ControlSettings.PowerElectronicsConnections' + }) + ''' + ''' + IEEE1547Info: Optional[str | IEEE1547Info] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547Info.PowerElectronicsConnections' + }) + ''' + ''' + IEEE1547Setting: Optional[str | IEEE1547Setting] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547Setting.PowerElectronicsConnections' + }) + ''' + ''' + IEEE1547TripSettings: Optional[str | IEEE1547TripSettings] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547TripSettings.PowerElectronicsConnections' + }) + ''' + ''' + PowerElectronicsConnectionPhases: list[str | PowerElectronicsConnectionPhase] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsConnectionPhase.PowerElectronicsConnection' + }) + ''' + ''' + PowerElectronicsUnit: list[str | PowerElectronicsUnit] = field( + default_factory=list, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': 'unbounded', + 'inverse': 'PowerElectronicsUnit.PowerElectronicsConnection' + }) + ''' + ''' + + +@dataclass(repr=False) +class RotatingMachine(ConductingEquipment): + ''' + A rotating machine which may be used as a generator or motor. + ''' + ratedPowerFactor: Optional[float] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Power factor (nameplate data). It is primarily used for short circuit data + exchange according to IEC 60909. + ''' + p: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Active power injection. Load sign convention is used, i.e. positive sign + means flow out from a node. + Starting value for a steady state solution. + ''' + q: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Reactive power injection. Load sign convention is used, i.e. positive sign + means flow out from a node. + Starting value for a steady state solution. + ''' + ratedS: Optional[float | ApparentPower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Nameplate apparent power rating for the unit. + The attribute shall have a positive value. + ''' + ratedU: Optional[float | Voltage] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Rated voltage (nameplate data, Ur in IEC 60909-0). It is primarily used + for short circuit data exchange according to IEC 60909. + ''' + GeneratingUnit: Optional[str | GeneratingUnit] = field(default=None, + metadata={ + 'type': + 'Association', + 'minOccurs': + '0', + 'maxOccurs': + '1', + 'inverse': + 'GeneratingUnit.RotatingMachine' + }) + ''' + A synchronous machine may operate as a generator and as such becomes a + member of a generating unit. + ''' + IEEE1547ControlSettings: Optional[str | IEEE1547ControlSettings] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547ControlSettings.RotatingMachines' + }) + ''' + ''' + IEEE1547Info: Optional[str | IEEE1547Info] = field(default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': + 'IEEE1547Info.RotatingMachines' + }) + ''' + ''' + IEEE1547Setting: Optional[str | IEEE1547Setting] = field(default=None, + metadata={ + 'type': + 'Association', + 'minOccurs': + '0', + 'maxOccurs': + '1', + 'inverse': + 'IEEE1547Setting.RotatingMachines' + }) + ''' + ''' + IEEE1547TripSettings: Optional[str | IEEE1547TripSettings] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'IEEE1547TripSettings.RotatingMachines' + }) + ''' + ''' + + +@dataclass(repr=False) +class GeneratingUnit(PowerSystemResource): + ''' + A single or set of synchronous machines for converting mechanical power + into alternating-current power. For example, individual machines within + a set may be defined for scheduling purposes while a single control signal + is derived for the set. In this case there would be a GeneratingUnit for + each member of the set and an additional GeneratingUnit corresponding to + the set. + ''' + RotatingMachine: list[str | RotatingMachine] = field(default_factory=list, + metadata={ + 'type': + 'Association', + 'minOccurs': + '0', + 'maxOccurs': + 'unbounded', + 'inverse': + 'RotatingMachine.GeneratingUnit' + }) + ''' + A synchronous machine may operate as a generator and as such becomes a + member of a generating unit. + ''' + + +@dataclass(repr=False) +class PowerElectronicsConnectionPhase(PowerSystemResource): + ''' + ''' + p: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Active power injection. Load sign convention is used, i.e. positive sign + means flow into the equipment from the network. + ''' + phase: Optional[str | SinglePhaseKind] = field(default=None, + metadata={ + 'type': 'Enumeration', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Phase of this energy producer component. If the energy producer is wye + connected, the connection is from the indicated phase to the central ground + or neutral point. If the energy producer is delta connected, the phase + indicates an energy producer connected from the indicated phase to the + next logical non-neutral phase. + ''' + q: Optional[float | ReactivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Reactive power injection. Load sign convention is used, i.e. positive sign + means flow into the equipment from the network. + ''' + PowerElectronicsConnection: Optional[str | PowerElectronicsConnection] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'PowerElectronicsConnection.PowerElectronicsConnectionPhases' + }) + ''' + ''' + + +@dataclass(repr=False) +class PowerElectronicsUnit(PowerSystemResource): + ''' + A generating unit or battery or aggregation that connects to the AC network + using power electronics rather than rotating machines. + ''' + maxP: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Maximum active power limit. This is the maximum (nameplate) limit for the + unit. + ''' + minP: Optional[float | ActivePower] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + Minimum active power limit. This is the minimum (nameplate) limit for the + unit. + ''' + PowerElectronicsConnection: Optional[str | PowerElectronicsConnection] = field( + default=None, + metadata={ + 'type': 'Association', + 'minOccurs': '0', + 'maxOccurs': '1', + 'inverse': 'PowerElectronicsConnection.PowerElectronicsUnit' + }) + ''' + ''' + + +@dataclass(repr=False) +class BatteryUnit(PowerElectronicsUnit): + ''' + An electrochemical energy storage device + ''' + batteryState: Optional[str | BatteryState] = field(default=None, + metadata={ + 'type': 'Enumeration', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + indicates whether the battery is charging, discharging or idle + ''' + ratedE: Optional[float | RealEnergy] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + full energy storage capacity of the battery + ''' + storedE: Optional[float | RealEnergy] = field(default=None, + metadata={ + 'type': 'Attribute', + 'minOccurs': '0', + 'maxOccurs': '1' + }) + ''' + amount of energy currently stored; no more than ratedE + ''' + + +@dataclass(repr=False) +class PhotoVoltaicUnit(PowerElectronicsUnit): + ''' + A photovoltaic device or an aggregation of such devices + ''' + + +@dataclass(repr=False) +class MeasurementValueQuality(Identity): + ''' + Measurement quality flags. Bits 0-10 are defined for substation automation + in draft IEC 61850 part 7-3. Bits 11-15 are reserved for future expansion + by that document. Bits 16-31 are reserved for EMS applications. + ''' + + +class BatteryState(Enum): + ''' + unable to Charge, and not Discharging + ''' + + +class IEEE1547AbnormalPerfomanceCategory(Enum): + ''' + ''' + + +class IEEE1547IslandingCategory(Enum): + ''' + See clause 8.2 + ''' + + +class IEEE1547NormalPerformanceCategory(Enum): + ''' + ''' + + +class PhaseCode(Enum): + ''' + An unordered enumeration of phase identifiers. Allows designation of phases + for both transmission and distribution equipment, circuits and loads. The + enumeration, by itself, does not describe how the phases are connected + together or connected to ground. Ground is not explicitly denoted as a + phase. + Residential and small commercial loads are often served from single-phase, + or split-phase, secondary circuits. For example of s12N, phases 1 and 2 + refer to hot wires that are 180 degrees out of phase, while N refers to + the neutral wire. Through single-phase transformer connections, these secondary + circuits may be served from one or two of the primary phases A, B, and + C. For three-phase loads, use the A, B, C phase codes instead of s12N. + ''' + A = 'A' + ''' + Phase A. + ''' + AB = 'AB' + ''' + Phases A and B. + ''' + ABC = 'ABC' + ''' + Phases A, B, and C. + ''' + ABCN = 'ABCN' + ''' + Phases A, B, C, and N. + ''' + ABN = 'ABN' + ''' + Phases A, B, and neutral. + ''' + AC = 'AC' + ''' + Phases A and C. + ''' + ACN = 'ACN' + ''' + Phases A, C and neutral. + ''' + AN = 'AN' + ''' + Phases A and neutral. + ''' + B = 'B' + ''' + Phase B. + ''' + BC = 'BC' + ''' + Phases B and C. + ''' + BCN = 'BCN' + ''' + Phases B, C, and neutral. + ''' + BN = 'BN' + ''' + Phases B and neutral. + ''' + C = 'C' + ''' + Phase C. + ''' + CN = 'CN' + ''' + Phases C and neutral. + ''' + N = 'N' + ''' + Neutral phase. + ''' + X = 'X' + ''' + Unknown non-neutral phase. + ''' + XN = 'XN' + ''' + Unknown non-neutral phase plus neutral. + ''' + XY = 'XY' + ''' + Two unknown non-neutral phases. + ''' + XYN = 'XYN' + ''' + Two unknown non-neutral phases plus neutral. + ''' + none = 'none' + ''' + No phases specified. + ''' + s1 = 's1' + ''' + Secondary phase 1. + ''' + s12 = 's12' + ''' + Secondary phase 1 and 2. + ''' + s12N = 's12N' + ''' + Secondary phases 1, 2, and neutral. + ''' + s1N = 's1N' + ''' + Secondary phase 1 and neutral. + ''' + s2 = 's2' + ''' + Secondary phase 2. + ''' + s2N = 's2N' + ''' + Secondary phase 2 and neutral. + ''' + + +class SinglePhaseKind(Enum): + ''' + Enumeration of single phase identifiers. Allows designation of single phases + for both transmission and distribution equipment, circuits and loads. + ''' + A = 'A' + ''' + Phase A. + ''' + B = 'B' + ''' + Phase B. + ''' + C = 'C' + ''' + Phase C. + ''' + N = 'N' + ''' + Neutral. + ''' + s1 = 's1' + ''' + Secondary phase 1. + ''' + s2 = 's2' + ''' + Secondary phase 2. + ''' + + +class SmartInverterMode(Enum): + ''' + Required to dispatch P and Q + ''' + + +@dataclass +class ReactivePower(): + value: float = field(default=None) + ''' + Product of RMS value of the voltage and the RMS value of the quadrature + component of the current. + ''' + + +@dataclass +class Susceptance(): + value: float = field(default=None) + ''' + Imaginary part of admittance. + ''' + + +@dataclass +class Seconds(): + value: float = field(default=None) + ''' + Time, in seconds. + ''' + + +@dataclass +class Voltage(): + value: float = field(default=None) + ''' + Electrical voltage, can be both AC and DC. + ''' + + +@dataclass +class ApparentPower(): + value: float = field(default=None) + ''' + Product of the RMS value of the voltage and the RMS value of the current. + ''' + + +@dataclass +class PerCent(): + value: float = field(default=None) + ''' + Percentage on a defined base. For example, specify as 100 to indicate at + the defined base. + ''' + + +@dataclass +class RealEnergy(): + value: float = field(default=None) + ''' + Real electrical energy. + ''' + + +@dataclass +class PU(): + value: float = field(default=None) + ''' + Per Unit - a positive or negative value referred to a defined base. Values + typically range from -10 to +10. + ''' + + +@dataclass +class Frequency(): + value: float = field(default=None) + ''' + Cycles per second. + ''' + + +@dataclass +class ActivePower(): + value: float = field(default=None) + ''' + Product of RMS value of the voltage and the RMS value of the in-phase component + of the current. + ''' diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/sep.py b/src/python/otsim/ieee_2030_5/client_helper/models/sep.py new file mode 100644 index 0000000..b125f4a --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/sep.py @@ -0,0 +1,8092 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import List, Optional + +__NAMESPACE__ = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActivePower: + """The active (real) power P (in W) is the product of root-mean-square + (RMS) voltage, RMS current, and cos(theta) where theta is the phase angle + of current relative to voltage. + + It is the primary measure of the rate of flow of energy. + + :ivar multiplier: Specifies exponent for uom. + :ivar value: Value in watts (uom 38) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class AmpereHour: + """ + Available electric charge. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in ampere-hours (uom 106) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ApparentPower: + """ + The apparent power S (in VA) is the product of root mean square (RMS) + voltage and RMS current. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in volt-amperes (uom 61) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ApplianceLoadReduction: + """The ApplianceLoadReduction object is used by a Demand Response service + provider to provide signals for ENERGY STAR compliant appliances. + + See the definition of ApplianceLoadReductionType for more + information. + + :ivar type: Indicates the type of appliance load reduction + requested. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + type: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class AppliedTargetReduction: + """ + Specifies the value of the TargetReduction applied by the device. + + :ivar type: Enumerated field representing the type of reduction + requested. + :ivar value: Indicates the requested amount of the relevant + commodity to be reduced. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + type: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Charge: + """Charges contain charges on a customer bill. + + These could be items like taxes, levies, surcharges, rebates, or + others. This is meant to allow the HAN device to retrieve enough + information to be able to reconstruct an estimate of what the total + bill would look like. Providers can provide line item billing, + including multiple charge kinds (e.g. taxes, surcharges) at whatever + granularity desired, using as many Charges as desired during a + billing period. There can also be any number of Charges associated + with different ReadingTypes to distinguish between TOU tiers, + consumption blocks, or demand charges. + + :ivar description: A description of the charge. + :ivar kind: The type (kind) of charge. + :ivar value: A monetary charge. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 20, + }) + kind: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Condition: + """ + Indicates a condition that must be satisfied for the Notification to be + triggered. + + :ivar attributeIdentifier: 0 = Reading value 1-255 = Reserved + :ivar lowerThreshold: The value of the lower threshold + :ivar upperThreshold: The value of the upper threshold + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + attributeIdentifier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + lowerThreshold: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + upperThreshold: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + + +@dataclass +class ConnectStatusType: + """DER ConnectStatus value (bitmap): + + 0 - Connected + 1 - Available + 2 - Operating + 3 - Test + 4 - Fault / Error + All other values reserved. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 1, + "format": "base16", + }) + + +@dataclass +class CreditTypeChange: + """ + Specifies a change to the credit type. + + :ivar newType: The new credit type, to take effect at the time + specified by startTime + :ivar startTime: The date/time when the change is to take effect. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + newType: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + startTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class CurrentRMS: + """ + Average flow of charge through a conductor. + + :ivar multiplier: Specifies exponent of value. + :ivar value: Value in amperes RMS (uom 5) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class CurveData: + """ + Data point values for defining a curve or schedule. + + :ivar excitation: If yvalue is Power Factor, then this field SHALL + be present. If yvalue is not Power Factor, then this field SHALL + NOT be present. True when DER is absorbing reactive power + (under-excited), false when DER is injecting reactive power + (over-excited). + :ivar xvalue: The data value of the X-axis (independent) variable, + depending on the curve type. See definitions in DERControlBase + for further information. + :ivar yvalue: The data value of the Y-axis (dependent) variable, + depending on the curve type. See definitions in DERControlBase + for further information. If yvalue is Power Factor, the + excitation field SHALL be present and yvalue SHALL be a positive + value. If yvalue is not Power Factor, the excitation field SHALL + NOT be present. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + excitation: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + xvalue: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + yvalue: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DateTimeInterval: + """ + Interval of date and time. + + :ivar duration: Duration of the interval, in seconds. + :ivar start: Date and time of the start of the interval. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + duration: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + start: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DutyCycle: + """Duty cycle control is a device specific issue and is managed by the + device. + + The duty cycle of the device under control should span the shortest + practical time period in accordance with the nature of the device + under control and the intent of the request for demand reduction. + The default factory setting SHOULD be three minutes for each 10% of + duty cycle. This indicates that the default time period over which + a duty cycle is applied is 30 minutes, meaning a 10% duty cycle + would cause a device to be ON for 3 minutes. The “off state” SHALL + precede the “on state”. + + :ivar normalValue: Contains the maximum On state duty cycle applied + by the end device, as a percentage of time. The field not + present indicates that this field has not been used by the end + device. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + normalValue: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class EnvironmentalCost: + """Provides alternative or secondary price information for the relevant + RateComponent. + + Supports jurisdictions that seek to convey the environmental price + per unit of the specified commodity not expressed in currency. + Implementers and consumers can use this attribute to prioritize + operations of their HAN devices (e.g., PEV charging during times of + high availability of renewable electricity resources). + + :ivar amount: The estimated or actual environmental or other cost, + per commodity unit defined by the ReadingType, for this + RateComponent (e.g., grams of carbon dioxide emissions each per + kWh). + :ivar costKind: The kind of cost referred to in the amount. + :ivar costLevel: The relative level of the amount attribute. In + conjunction with numCostLevels, this attribute informs a device + of the relative scarcity of the amount attribute (e.g., a high + or low availability of renewable generation). numCostLevels and + costLevel values SHALL ascend in order of scarcity, where "0" + signals the lowest relative cost and higher values signal + increasing cost. For example, if numCostLevels is equal to “3,” + then if the lowest relative costLevel were equal to “0,” devices + would assume this is the lowest relative period to operate. + Likewise, if the costLevel in the next TimeTariffInterval + instance is equal to “1,” then the device would assume it is + relatively more expensive, in environmental terms, to operate + during this TimeTariffInterval instance than the previous one. + There is no limit to the number of relative price levels other + than that indicated in the attribute type, but for practicality, + service providers should strive for simplicity and recognize the + diminishing returns derived from increasing the numCostLevel + value greater than four. + :ivar numCostLevels: The number of all relative cost levels. In + conjunction with costLevel, numCostLevels signals the relative + scarcity of the commodity for the duration of the + TimeTariffInterval instance (e.g., a relative indication of + cost). This is useful in providing context for nominal cost + signals to consumers or devices that might see a range of amount + values from different service providres or from the same service + provider. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + amount: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + costKind: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + costLevel: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + numCostLevels: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Error: + """ + Contains information about the nature of an error if a request could not be + completed successfully. + + :ivar maxRetryDuration: Contains the number of seconds the client + SHOULD wait before retrying the request. + :ivar reasonCode: Code indicating the reason for failure. 0 - + Invalid request format 1 - Invalid request values (e.g. invalid + threshold values) 2 - Resource limit reached 3 - Conditional + subscription field not supported 4 - Maximum request frequency + exceeded All other values reserved + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + maxRetryDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + reasonCode: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class EventStatus: + """Current status information relevant to a specific object. + + The Status object is used to indicate the current status of an + Event. Devices can read the containing resource (e.g. TextMessage) + to get the most up to date status of the event. Devices can also + subscribe to a specific resource instance to get updates when any of + its attributes change, including the Status object. + + :ivar currentStatus: Field representing the current status type. 0 = + Scheduled This status indicates that the event has been + scheduled and the event has not yet started. The server SHALL + set the event to this status when the event is first scheduled + and persist until the event has become active or has been + cancelled. For events with a start time less than or equal to + the current time, this status SHALL never be indicated, the + event SHALL start with a status of “Active”. 1 = Active This + status indicates that the event is currently active. The server + SHALL set the event to this status when the event reaches its + earliest Effective Start Time. 2 = Cancelled When events are + cancelled, the Status.dateTime attribute SHALL be set to the + time the cancellation occurred, which cannot be in the future. + The server is responsible for maintaining the cancelled event in + its collection for the duration of the original event, or until + the server has run out of space and needs to store a new event. + Client devices SHALL be aware of Cancelled events, determine if + the Cancelled event applies to them, and cancel the event + immediately if applicable. 3 = Cancelled with Randomization The + server is responsible for maintaining the cancelled event in its + collection for the duration of the Effective Scheduled Period. + Client devices SHALL be aware of Cancelled with Randomization + events, determine if the Cancelled event applies to them, and + cancel the event immediately, using the larger of (absolute + value of randomizeStart) and (absolute value of + randomizeDuration) as the end randomization, in seconds. This + Status.type SHALL NOT be used with "regular" Events, only with + specializations of RandomizableEvent. 4 = Superseded Events + marked as Superseded by servers are events that may have been + replaced by new events from the same program that target the + exact same set of deviceCategory's (if applicable) AND + DERControl controls (e.g., opModTargetW) (if applicable) and + overlap for a given period of time. Servers SHALL mark an event + as Superseded at the earliest Effective Start Time of the + overlapping event. Servers are responsible for maintaining the + Superseded event in their collection for the duration of the + Effective Scheduled Period. Client devices encountering a + Superseded event SHALL terminate execution of the event + immediately and commence execution of the new event immediately, + unless the current time is within the start randomization window + of the superseded event, in which case the client SHALL obey the + start randomization of the new event. This Status.type SHALL NOT + be used with TextMessage, since multiple text messages can be + active. All other values reserved. + :ivar dateTime: The dateTime attribute will provide a timestamp of + when the current status was defined. dateTime MUST be set to the + time at which the status change occurred, not a time in the + future or past. + :ivar potentiallySuperseded: Set to true by a server of this event + if there are events that overlap this event in time and also + overlap in some, but not all, deviceCategory's (if applicable) + AND DERControl controls (e.g., opModTargetW) (if applicable) in + the same function set instance. + :ivar potentiallySupersededTime: Indicates the time that the + potentiallySuperseded flag was set. + :ivar reason: The Reason attribute allows a Service provider to + provide a textual explanation of the status. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + currentStatus: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + potentiallySuperseded: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + potentiallySupersededTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + reason: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 192, + }) + + +@dataclass +class FixedPointType: + """ + Abstract type for specifying a fixed-point value without a given unit of + measure. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Dimensionless value + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class FixedVar: + """ + Specifies a signed setpoint for reactive power. + + :ivar refType: Indicates whether to interpret 'value' as %setMaxVar + or %statVarAvail. + :ivar value: Specify a signed setpoint for reactive power in % (see + 'refType' for context). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + refType: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class FreqDroopType: + """ + Type for Frequency-Droop (Frequency-Watt) operation. + + :ivar dBOF: Frequency droop dead band for over-frequency conditions. + In thousandths of Hz. + :ivar dBUF: Frequency droop dead band for under-frequency + conditions. In thousandths of Hz. + :ivar kOF: Frequency droop per-unit frequency change for over- + frequency conditions corresponding to 1 per-unit power output + change. In thousandths, unitless. + :ivar kUF: Frequency droop per-unit frequency change for under- + frequency conditions corresponding to 1 per-unit power output + change. In thousandths, unitless. + :ivar openLoopTms: Open loop response time, the duration from a step + change in control signal input until the output changes by 90% + of its final change before any overshoot, in hundredths of a + second. Resolution is 1/100 sec. A value of 0 is used to mean no + limit. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dBOF: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + dBUF: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + kOF: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + kUF: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + openLoopTms: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class GPSLocationType: + """ + Specifies a GPS location, expressed in WGS 84 coordinates. + + :ivar lat: Specifies the latitude from equator. -90 (south) to +90 + (north) in decimal degrees. + :ivar lon: Specifies the longitude from Greenwich Meridian. -180 + (west) to +180 (east) in decimal degrees. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + lat: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + lon: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + + +@dataclass +class InverterStatusType: + """DER InverterStatus value: + + 0 - N/A + 1 - off + 2 - sleeping (auto-shutdown) or DER is at low output power/voltage + 3 - starting up or ON but not producing power + 4 - tracking MPPT power point + 5 - forced power reduction/derating + 6 - shutting down + 7 - one or more faults exist + 8 - standby (service on unit) - DER may be at high output voltage/power + 9 - test mode + 10 - as defined in manufacturer status + All other values reserved. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Link: + """ + Links provide a reference, via URI, to another resource. + + :ivar href: A URI reference. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + href: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class LocalControlModeStatusType: + """DER LocalControlModeStatus/value: + + 0 – local control 1 – remote control All other values reserved. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ManufacturerStatusType: + """ + DER ManufacturerStatus/value: String data type. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 6, + }) + + +@dataclass +class Offset: + """If a temperature offset is sent that causes the heating or cooling + temperature set point to exceed the limit boundaries that are programmed + into the device, the device SHALL respond by setting the temperature at the + limit. + + If an EDC is being targeted at multiple devices or to a device that + controls multiple devices (e.g., EMS), it can provide multiple + Offset types within one EDC. For events with multiple Offset types, + a client SHALL select the Offset that best fits their operating + function. Alternatively, an event with a single Offset type can be + targeted at an EMS in order to request a percentage load reduction + on the average energy usage of the entire premise. An EMS SHOULD use + the Metering function set to determine the initial load in the + premise, reduce energy consumption by controlling devices at its + disposal, and at the conclusion of the event, once again use the + Metering function set to determine if the desired load reduction was + achieved. + + :ivar coolingOffset: The value change requested for the cooling + offset, in degree C / 10. The value should be added to the + normal set point for cooling, or if loadShiftForward is true, + then the value should be subtracted from the normal set point. + :ivar heatingOffset: The value change requested for the heating + offset, in degree C / 10. The value should be subtracted for + heating, or if loadShiftForward is true, then the value should + be added to the normal set point. + :ivar loadAdjustmentPercentageOffset: The value change requested for + the load adjustment percentage. The value should be subtracted + from the normal setting, or if loadShiftForward is true, then + the value should be added to the normal setting. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + coolingOffset: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + heatingOffset: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + loadAdjustmentPercentageOffset: Optional[int] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class OperationalModeStatusType: + """DER OperationalModeStatus value: + + 0 - Not applicable / Unknown + 1 - Off + 2 - Operational mode + 3 - Test mode + All other values reserved. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PowerConfiguration: + """ + Contains configuration related to the device's power sources. + + :ivar batteryInstallTime: Time/Date at which battery was installed, + :ivar lowChargeThreshold: In context of the PowerStatus resource, + this is the value of EstimatedTimeRemaining below which + BatteryStatus "low" is indicated and the PS_LOW_BATTERY is + raised. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + batteryInstallTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + lowChargeThreshold: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class PowerFactor: + """ + Specifies a setpoint for Displacement Power Factor, the ratio between + apparent and active powers at the fundamental frequency (e.g. 60 Hz). + + :ivar displacement: Significand of an unsigned value of cos(theta) + between 0 and 1.0. E.g. a value of 0.95 may be specified as a + displacement of 950 and a multiplier of -3. + :ivar multiplier: Specifies exponent of 'displacement'. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + displacement: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PowerFactorWithExcitation: + """ + Specifies a setpoint for Displacement Power Factor, the ratio between + apparent and active powers at the fundamental frequency (e.g. 60 Hz) and + includes an excitation flag. + + :ivar displacement: Significand of an unsigned value of cos(theta) + between 0 and 1.0. E.g. a value of 0.95 may be specified as a + displacement of 950 and a multiplier of -3. + :ivar excitation: True when DER is absorbing reactive power (under- + excited), false when DER is injecting reactive power (over- + excited). + :ivar multiplier: Specifies exponent of 'displacement'. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + displacement: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + excitation: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ReactivePower: + """ + The reactive power Q (in var) is the product of root mean square (RMS) + voltage, RMS current, and sin(theta) where theta is the phase angle of + current relative to voltage. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in volt-amperes reactive (var) (uom 63) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ReactiveSusceptance: + """ + Reactive susceptance. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in siemens (uom 53) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class RealEnergy: + """ + Real electrical energy. + + :ivar multiplier: Multiplier for 'unit'. + :ivar value: Value of the energy in Watt-hours. (uom 72) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_inclusive": 281474976710655, + }) + + +@dataclass +class RequestStatus: + """ + The RequestStatus object is used to indicate the current status of a Flow + Reservation Request. + + :ivar dateTime: The dateTime attribute will provide a timestamp of + when the request status was set. dateTime MUST be set to the + time at which the status change occurred, not a time in the + future or past. + :ivar requestStatus: Field representing the request status type. 0 = + Requested 1 = Cancelled All other values reserved. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + requestStatus: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Resource: + """ + A resource is an addressable unit of information, either a collection + (List) or instance of an object (identifiedObject, or simply, Resource) + + :ivar href: A reference to the resource address (URI). Required in a + response to a GET, ignored otherwise. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + href: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + }) + + +@dataclass +class ServiceChange: + """ + Specifies a change to the service status. + + :ivar newStatus: The new service status, to take effect at the time + specified by startTime + :ivar startTime: The date/time when the change is to take effect. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + newStatus: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + startTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class SetPoint: + """The SetPoint object is used to apply specific temperature set points to + a temperature control device. + + The values of the heatingSetpoint and coolingSetpoint attributes SHALL be calculated as follows: + Cooling/Heating Temperature Set Point / 100 = temperature in degrees Celsius where -273.15°C &lt;= temperature &lt;= 327.67°C, corresponding to a Cooling and/or Heating Temperature Set Point. The maximum resolution this format allows is 0.01°C. + The field not present in a Response indicates that this field has not been used by the end device. + If a temperature is sent that exceeds the temperature limit boundaries that are programmed into the device, the device SHALL respond by setting the temperature at the limit. + + :ivar coolingSetpoint: This attribute represents the cooling + temperature set point in degrees Celsius / 100. (Hundredths of a + degree C) + :ivar heatingSetpoint: This attribute represents the heating + temperature set point in degrees Celsius / 100. (Hundredths of a + degree C) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + coolingSetpoint: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + heatingSetpoint: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class SignedRealEnergy: + """ + Real electrical energy, signed. + + :ivar multiplier: Multiplier for 'unit'. + :ivar value: Value of the energy in Watt-hours. (uom 72) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + + +@dataclass +class StateOfChargeStatusType: + """ + DER StateOfChargeStatus value: Percent data type. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class StorageModeStatusType: + """DER StorageModeStatus value: + + 0 – storage charging 1 – storage discharging 2 – storage holding All + other values reserved. + + :ivar dateTime: The date and time at which the state applied. + :ivar value: The value indicating the state. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class TargetReduction: + """The TargetReduction object is used by a Demand Response service provider + to provide a RECOMMENDED threshold that a device/premises should maintain + its consumption below. + + For example, a service provider can provide a RECOMMENDED threshold + of some kWh for a 3-hour event. This means that the device/premises + would maintain its consumption below the specified limit for the + specified period. + + :ivar type: Indicates the type of reduction requested. + :ivar value: Indicates the requested amount of the relevant + commodity to be reduced. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + type: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class Temperature: + """ + Specification of a temperature. + + :ivar multiplier: Multiplier for 'unit'. + :ivar subject: The subject of the temperature measurement 0 - + Enclosure 1 - Transformer 2 - HeatSink + :ivar value: Value in Degrees Celsius (uom 23). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + subject: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class TimeConfiguration: + """ + Contains attributes related to the configuration of the time service. + + :ivar dstEndRule: Rule to calculate end of daylight savings time in + the current year. Result of dstEndRule must be greater than + result of dstStartRule. + :ivar dstOffset: Daylight savings time offset from local standard + time. + :ivar dstStartRule: Rule to calculate start of daylight savings time + in the current year. Result of dstEndRule must be greater than + result of dstStartRule. + :ivar tzOffset: Local time zone offset from UTCTime. Does not + include any daylight savings time offsets. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dstEndRule: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 4, + "format": "base16", + }) + dstOffset: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + dstStartRule: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 4, + "format": "base16", + }) + tzOffset: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class UnitValueType: + """ + Type for specification of a specific value, with units and power of ten + multiplier. + + :ivar multiplier: Multiplier for 'unit'. + :ivar unit: Unit in symbol + :ivar value: Value in units specified + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + unit: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class UnsignedFixedPointType: + """ + Abstract type for specifying an unsigned fixed-point value without a given + unit of measure. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Dimensionless value + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class VoltageRMS: + """ + Average electric potential difference between two points. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in volts RMS (uom 29) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class WattHour: + """ + Active (real) energy. + + :ivar multiplier: Specifies exponent of uom. + :ivar value: Value in watt-hours (uom 72) + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class loWPAN: + """ + Contains information specific to 6LoWPAN. + + :ivar octetsRx: Number of Bytes received + :ivar octetsTx: Number of Bytes transmitted + :ivar packetsRx: Number of packets received + :ivar packetsTx: Number of packets transmitted + :ivar rxFragError: Number of errors receiving fragments + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + octetsRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + octetsTx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + packetsRx: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + packetsTx: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + rxFragError: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class AccountBalanceLink(Link): + """ + SHALL contain a Link to an instance of AccountBalance. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class AccountingUnit: + """ + Unit for accounting; use either 'energyUnit' or 'currencyUnit' to specify + the unit for 'value'. + + :ivar energyUnit: Unit of service. + :ivar monetaryUnit: Unit of currency. + :ivar multiplier: Multiplier for the 'energyUnit' or 'monetaryUnit'. + :ivar value: Value of the monetary aspect + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + energyUnit: Optional[RealEnergy] = field(default=None, metadata={ + "type": "Element", + }) + monetaryUnit: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + multiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + value: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class AssociatedUsagePointLink(Link): + """SHALL contain a Link to an instance of UsagePoint. + + If present, this is the submeter that monitors the DER output. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class BillingPeriod(Resource): + """A Billing Period relates to the period of time on which a customer is + billed. + + As an example the billing period interval for a particular customer + might be 31 days starting on July 1, 2011. The start date and + interval can change on each billing period. There may also be + multiple billing periods related to a customer agreement to support + different tariff structures. + + :ivar billLastPeriod: The amount of the bill for the previous + billing period. + :ivar billToDate: The bill amount related to the billing period as + of the statusTimeStamp. + :ivar interval: The time interval for this billing period. + :ivar statusTimeStamp: The date / time of the last update of this + resource. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + billLastPeriod: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + billToDate: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + interval: Optional[DateTimeInterval] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + statusTimeStamp: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class ConfigurationLink(Link): + """ + SHALL contain a Link to an instance of Configuration. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ConsumptionTariffInterval(Resource): + """One of a sequence of thresholds defined in terms of consumption quantity + of a service such as electricity, water, gas, etc. + + It defines the steps or blocks in a step tariff structure, where + startValue simultaneously defines the entry value of this step and + the closing value of the previous step. Where consumption is greater + than startValue, it falls within this block and where consumption is + less than or equal to startValue, it falls within one of the + previous blocks. + + :ivar consumptionBlock: Indicates the consumption block related to + the reading. If not specified, is assumed to be "0 - N/A". + :ivar EnvironmentalCost: + :ivar price: The charge for this rate component, per unit of measure + defined by the associated ReadingType, in currency specified in + TariffProfile. The Pricing service provider determines the + appropriate price attribute value based on its applicable + regulatory rules. For example, price could be net or inclusive + of applicable taxes, fees, or levies. The Billing function set + provides the ability to represent billing information in a more + detailed manner. + :ivar startValue: The lowest level of consumption that defines the + starting point of this consumption step or block. Thresholds + start at zero for each billing period. If specified, the first + ConsumptionTariffInterval.startValue for a TimeTariffInteral + instance SHALL begin at "0." Subsequent + ConsumptionTariffInterval.startValue elements SHALL be greater + than the previous one. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + consumptionBlock: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + EnvironmentalCost: List[EnvironmentalCost] = field(default_factory=list, + metadata={ + "type": "Element", + }) + price: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + startValue: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_inclusive": 281474976710655, + }) + + +@dataclass +class CurrentDERProgramLink(Link): + """SHALL contain a Link to an instance of DERProgram. + + If present, this is the DERProgram containing the currently active + DERControl. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class CustomerAccountLink(Link): + """ + SHALL contain a Link to an instance of CustomerAccount. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERAvailabilityLink(Link): + """ + SHALL contain a Link to an instance of DERAvailability. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERCapability(Resource): + """ + Distributed energy resource type and nameplate ratings. + + :ivar modesSupported: Bitmap indicating the DER Controls implemented + by the device. See DERControlType for values. + :ivar rtgAbnormalCategory: Abnormal operating performance category + as defined by IEEE 1547-2018. One of: 0 - not specified 1 - + Category I 2 - Category II 3 - Category III All other values + reserved. + :ivar rtgMaxA: Maximum continuous AC current capability of the DER, + in Amperes (RMS). + :ivar rtgMaxAh: Usable energy storage capacity of the DER, in + AmpHours. + :ivar rtgMaxChargeRateVA: Maximum apparent power charge rating in + Volt-Amperes. May differ from the maximum apparent power rating. + :ivar rtgMaxChargeRateW: Maximum rate of energy transfer received by + the storage DER, in Watts. + :ivar rtgMaxDischargeRateVA: Maximum apparent power discharge rating + in Volt-Amperes. May differ from the maximum apparent power + rating. + :ivar rtgMaxDischargeRateW: Maximum rate of energy transfer + delivered by the storage DER, in Watts. Required for combined + generation/storage DERs (e.g. DERType == 83). + :ivar rtgMaxV: AC voltage maximum rating. + :ivar rtgMaxVA: Maximum continuous apparent power output capability + of the DER, in VA. + :ivar rtgMaxVar: Maximum continuous reactive power delivered by the + DER, in var. + :ivar rtgMaxVarNeg: Maximum continuous reactive power received by + the DER, in var. If absent, defaults to negative rtgMaxVar. + :ivar rtgMaxW: Maximum continuous active power output capability of + the DER, in watts. Represents combined generation plus storage + output if DERType == 83. + :ivar rtgMaxWh: Maximum energy storage capacity of the DER, in + WattHours. + :ivar rtgMinPFOverExcited: Minimum Power Factor displacement + capability of the DER when injecting reactive power (over- + excited); SHALL be a positive value between 0.0 (typically + &gt; 0.7) and 1.0. If absent, defaults to unity. + :ivar rtgMinPFUnderExcited: Minimum Power Factor displacement + capability of the DER when absorbing reactive power (under- + excited); SHALL be a positive value between 0.0 (typically + &gt; 0.7) and 0.9999. If absent, defaults to + rtgMinPFOverExcited. + :ivar rtgMinV: AC voltage minimum rating. + :ivar rtgNormalCategory: Normal operating performance category as + defined by IEEE 1547-2018. One of: 0 - not specified 1 - + Category A 2 - Category B All other values reserved. + :ivar rtgOverExcitedPF: Specified over-excited power factor. + :ivar rtgOverExcitedW: Active power rating in Watts at specified + over-excited power factor (rtgOverExcitedPF). If present, + rtgOverExcitedPF SHALL be present. + :ivar rtgReactiveSusceptance: Reactive susceptance that remains + connected to the Area EPS in the cease to energize and trip + state. + :ivar rtgUnderExcitedPF: Specified under-excited power factor. + :ivar rtgUnderExcitedW: Active power rating in Watts at specified + under-excited power factor (rtgUnderExcitedPF). If present, + rtgUnderExcitedPF SHALL be present. + :ivar rtgVNom: AC voltage nominal rating. + :ivar type: Type of DER; see DERType object + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + modesSupported: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 4, + "format": "base16", + }) + rtgAbnormalCategory: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxA: Optional[CurrentRMS] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxAh: Optional[AmpereHour] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxChargeRateVA: Optional[ApparentPower] = field(default=None, + metadata={ + "type": "Element", + }) + rtgMaxChargeRateW: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxDischargeRateVA: Optional[ApparentPower] = field(default=None, + metadata={ + "type": "Element", + }) + rtgMaxDischargeRateW: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + }) + rtgMaxV: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxVA: Optional[ApparentPower] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxVar: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxVarNeg: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + rtgMaxW: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + rtgMaxWh: Optional[WattHour] = field(default=None, metadata={ + "type": "Element", + }) + rtgMinPFOverExcited: Optional[PowerFactor] = field(default=None, + metadata={ + "type": "Element", + }) + rtgMinPFUnderExcited: Optional[PowerFactor] = field(default=None, + metadata={ + "type": "Element", + }) + rtgMinV: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + rtgNormalCategory: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + rtgOverExcitedPF: Optional[PowerFactor] = field(default=None, metadata={ + "type": "Element", + }) + rtgOverExcitedW: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + rtgReactiveSusceptance: Optional[ReactiveSusceptance] = field(default=None, + metadata={ + "type": "Element", + }) + rtgUnderExcitedPF: Optional[PowerFactor] = field(default=None, metadata={ + "type": "Element", + }) + rtgUnderExcitedW: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + rtgVNom: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + type: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DERCapabilityLink(Link): + """ + SHALL contain a Link to an instance of DERCapability. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERCurveLink(Link): + """ + SHALL contain a Link to an instance of DERCurve. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERLink(Link): + """ + SHALL contain a Link to an instance of DER. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERProgramLink(Link): + """ + SHALL contain a Link to an instance of DERProgram. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERSettingsLink(Link): + """ + SHALL contain a Link to an instance of DERSettings. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERStatusLink(Link): + """ + SHALL contain a Link to an instance of DERStatus. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DRLCCapabilities: + """ + Contains information about the static capabilities of the device, to allow + service providers to know what types of functions are supported, what the + normal operating ranges and limits are, and other similar information, in + order to provide better suggestions of applicable programs to receive the + maximum benefit. + + :ivar averageEnergy: The average hourly energy usage when in normal + operating mode. + :ivar maxDemand: The maximum demand rating of this end device. + :ivar optionsImplemented: Bitmap indicating the DRLC options + implemented by the device. 0 - Target reduction (kWh) 1 - Target + reduction (kW) 2 - Target reduction (Watts) 3 - Target reduction + (Cubic Meters) 4 - Target reduction (Cubic Feet) 5 - Target + reduction (US Gallons) 6 - Target reduction (Imperial Gallons) 7 + - Target reduction (BTUs) 8 - Target reduction (Liters) 9 - + Target reduction (kPA (gauge)) 10 - Target reduction (kPA + (absolute)) 11 - Target reduction (Mega Joule) 12 - Target + reduction (Unitless) 13-15 - Reserved 16 - Temperature set point + 17 - Temperature offset 18 - Duty cycle 19 - Load adjustment + percentage 20 - Appliance load reduction 21-31 - Reserved + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + averageEnergy: Optional[RealEnergy] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + maxDemand: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + optionsImplemented: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 4, + "format": "base16", + }) + + +@dataclass +class DefaultDERControlLink(Link): + """SHALL contain a Link to an instance of DefaultDERControl. + + This is the default mode of the DER which MAY be overridden by + DERControl events. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DemandResponseProgramLink(Link): + """ + SHALL contain a Link to an instance of DemandResponseProgram. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DeviceCapabilityLink(Link): + """ + SHALL contain a Link to an instance of DeviceCapability. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DeviceInformationLink(Link): + """ + SHALL contain a Link to an instance of DeviceInformation. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DeviceStatusLink(Link): + """ + SHALL contain a Link to an instance of DeviceStatus. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class EndDeviceLink(Link): + """ + SHALL contain a Link to an instance of EndDevice. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class File(Resource): + """This resource contains various meta-data describing a file's + characteristics. + + The meta-data provides general file information and also is used to + support filtered queries of file lists + + :ivar activateTime: This element MUST be set to the date/time at + which this file is activated. If the activation time is less + than or equal to current time, the LD MUST immediately place the + file into the activated state (in the case of a firmware file, + the file is now the running image). If the activation time is + greater than the current time, the LD MUST wait until the + specified activation time is reached, then MUST place the file + into the activated state. Omission of this element means that + the LD MUST NOT take any action to activate the file until a + subsequent GET to this File resource provides an activateTime. + :ivar fileURI: This element MUST be set to the URI location of the + file binary artifact. This is the BLOB (binary large object) + that is actually loaded by the LD + :ivar lFDI: This element MUST be set to the LFDI of the device for + which this file in targeted. + :ivar mfHwVer: This element MUST be set to the hardware version for + which this file is targeted. + :ivar mfID: This element MUST be set to the manufacturer's Private + Enterprise Number (assigned by IANA). + :ivar mfModel: This element MUST be set to the manufacturer model + number for which this file is targeted. The syntax and semantics + are left to the manufacturer. + :ivar mfSerNum: This element MUST be set to the manufacturer serial + number for which this file is targeted. The syntax and semantics + are left to the manufacturer. + :ivar mfVer: This element MUST be set to the software version + information for this file. The syntax and semantics are left to + the manufacturer. + :ivar size: This element MUST be set to the total size (in bytes) of + the file referenced by fileURI. + :ivar type: A value indicating the type of the file. MUST be one of + the following values: 00 = Software Image 01 = Security + Credential 02 = Configuration 03 = Log 04–7FFF = reserved + 8000-FFFF = Manufacturer defined + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + activateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + fileURI: Optional[str] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + lFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 20, + "format": "base16", + }) + mfHwVer: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 32, + }) + mfID: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + mfModel: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + mfSerNum: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 32, + }) + mfVer: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + }) + size: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + type: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 2, + "format": "base16", + }) + + +@dataclass +class FileLink(Link): + """This element MUST be set to the URI of the most recent File being + loaded/activated by the LD. + + In the case of file status 0, this element MUST be omitted. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FileStatusLink(Link): + """ + SHALL contain a Link to an instance of FileStatus. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class IdentifiedObject(Resource): + """ + This is a root class to provide common naming attributes for all classes + needing naming attributes. + + :ivar mRID: The global identifier of the object. + :ivar description: The description is a human readable text + describing or naming the object. + :ivar version: Contains the version number of the object. See the + type definition for details. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + mRID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + version: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class List_type(Resource): + """Container to hold a collection of object instances or references. + + See Design Pattern section for additional details. + + :ivar all: The number specifying "all" of the items in the list. + Required on a response to a GET, ignored otherwise. + :ivar results: Indicates the number of items in this page of + results. + """ + + class Meta: + name = "List" + namespace = "urn:ieee:std:2030.5:ns" + + all: Optional[int] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + results: Optional[int] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class ListLink(Link): + """ + ListLinks provide a reference, via URI, to a List. + + :ivar all: Indicates the total number of items in the referenced + list. This attribute SHALL be present if the href is a local or + relative URI. This attribute SHOULD NOT be present if the href + is a remote or absolute URI, as the server may be unaware of + changes to the value. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + all: Optional[int] = field(default=None, metadata={ + "type": "Attribute", + }) + + +@dataclass +class LogEvent(Resource): + """ + A time stamped instance of a significant event detected by the device. + + :ivar createdDateTime: The date and time that the event occurred. + :ivar details: Human readable text that MAY be used to transmit + additional details about the event. A host MAY remove this field + when received. + :ivar extendedData: May be used to transmit additional details about + the event. + :ivar functionSet: If the profileID indicates this is IEEE 2030.5, + the functionSet is defined by IEEE 2030.5 and SHALL be one of + the values from the table below (IEEE 2030.5 function set + identifiers). If the profileID is anything else, the functionSet + is defined by the identified profile. 0 General (not + specific to a function set) 1 Publish and Subscribe 2 + End Device 3 Function Set Assignment 4 Response 5 + Demand Response and Load Control 6 Metering 7 + Pricing 8 Messaging 9 Billing 10 Prepayment 11 + Distributed Energy Resources 12 Time 13 Software + Download 14 Device Information 15 Power Status 16 + Network Status 17 Log Event List 18 Configuration 19 + Security All other values are reserved. + :ivar logEventCode: An 8 bit unsigned integer. logEventCodes are + scoped to a profile and a function set. If the profile is IEEE + 2030.5, the logEventCode is defined by IEEE 2030.5 within one of + the function sets of IEEE 2030.5. If the profile is anything + else, the logEventCode is defined by the specified profile. + :ivar logEventID: This 16-bit value, combined with createdDateTime, + profileID, and logEventPEN, should provide a reasonable level of + uniqueness. + :ivar logEventPEN: The Private Enterprise Number(PEN) of the entity + that defined the profileID, functionSet, and logEventCode of the + logEvent. IEEE 2030.5-assigned logEventCodes SHALL use the IEEE + 2030.5 PEN. Combinations of profileID, functionSet, and + logEventCode SHALL have unique meaning within a logEventPEN and + are defined by the owner of the PEN. + :ivar profileID: The profileID identifies which profile (HA, BA, SE, + etc) defines the following event information. 0 Not + profile specific. 1 Vendor Defined 2 IEEE 2030.5 3 + Home Automation 4 Building Automation All other values are + reserved. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + createdDateTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + details: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 32, + }) + extendedData: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + functionSet: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + logEventCode: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + logEventID: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + logEventPEN: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + profileID: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class MeterReadingLink(Link): + """ + SHALL contain a Link to an instance of MeterReading. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class Neighbor(Resource): + """ + Contains 802.15.4 link layer specific attributes. + + :ivar isChild: True if the neighbor is a child. + :ivar linkQuality: The quality of the link, as defined by 802.15.4 + :ivar shortAddress: As defined by IEEE 802.15.4 + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + isChild: Optional[bool] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + linkQuality: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + shortAddress: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PEVInfo: + """ + Contains attributes that can be exposed by PEVs and other devices that have + charging requirements. + + :ivar chargingPowerNow: This is the actual power flow in or out of + the charger or inverter. This is calculated by the vehicle based + on actual measurements. This number is positive for charging. + :ivar energyRequestNow: This is the amount of energy that must be + transferred from the grid to EVSE and PEV to achieve the target + state of charge allowing for charger efficiency and any vehicle + and EVSE parasitic loads. This is calculated by the vehicle and + changes throughout the connection as forward or reverse power + flow change the battery state of charge. This number is + positive for charging. + :ivar maxForwardPower: This is maximum power transfer capability + that could be used for charging the PEV to perform the requested + energy transfer. It is the lower of the vehicle or EVSE + physical power limitations. It is not based on economic + considerations. The vehicle may draw less power than this value + based on its charging cycle. The vehicle defines this parameter. + This number is positive for charging power flow. + :ivar minimumChargingDuration: This is computed by the PEV based on + the charging profile to complete the energy transfer if the + maximum power is authorized. The value will never be smaller + than the ratio of the energy request to the power request + because the charging profile may not allow the maximum power to + be used throughout the transfer. This is a critical parameter + for determining whether any slack time exists in the charging + cycle between the current time and the TCIN. + :ivar targetStateOfCharge: This is the target state of charge that + is to be achieved during charging before the time of departure + (TCIN). The default value is 100%. The value cannot be set to a + value less than the actual state of charge. + :ivar timeChargeIsNeeded: Time Charge is Needed (TCIN) is the time + that the PEV is expected to depart. The value is manually + entered using controls and displays in the vehicle or on the + EVSE or using a mobile device. It is authenticated and saved by + the PEV. This value may be updated during a charging session. + :ivar timeChargingStatusPEV: This is the time that the parameters + are updated, except for changes to TCIN. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + chargingPowerNow: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + energyRequestNow: Optional[RealEnergy] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + maxForwardPower: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + minimumChargingDuration: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + targetStateOfCharge: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + timeChargeIsNeeded: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + timeChargingStatusPEV: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PowerStatusLink(Link): + """ + SHALL contain a Link to an instance of PowerStatus. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class PrepayOperationStatus(Resource): + """ + PrepayOperationStatus describes the status of the service or commodity + being conditionally controlled by the Prepayment function set. + + :ivar creditTypeChange: CreditTypeChange is used to define a pending + change of creditTypeInUse, which will activate at a specified + time. + :ivar creditTypeInUse: CreditTypeInUse identifies whether the + present mode of operation is consuming regular credit or + emergency credit. + :ivar serviceChange: ServiceChange is used to define a pending + change of serviceStatus, which will activate at a specified + time. + :ivar serviceStatus: ServiceStatus identifies whether the service is + connected or disconnected, or armed for connection or + disconnection. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + creditTypeChange: Optional[CreditTypeChange] = field(default=None, + metadata={ + "type": "Element", + }) + creditTypeInUse: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + serviceChange: Optional[ServiceChange] = field(default=None, metadata={ + "type": "Element", + }) + serviceStatus: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PrepayOperationStatusLink(Link): + """ + SHALL contain a Link to an instance of PrepayOperationStatus. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class PrepaymentLink(Link): + """ + SHALL contain a Link to an instance of Prepayment. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RPLSourceRoutes(Resource): + """ + A RPL source routes object. + + :ivar DestAddress: See [RFC 6554]. + :ivar SourceRoute: See [RFC 6554]. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DestAddress: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + SourceRoute: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + + +@dataclass +class RateComponentLink(Link): + """ + SHALL contain a Link to an instance of RateComponent. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ReadingBase(Resource): + """Specific value measured by a meter or other asset. + + ReadingBase is abstract, used to define the elements common to + Reading and IntervalReading. + + :ivar consumptionBlock: Indicates the consumption block related to + the reading. REQUIRED if ReadingType numberOfConsumptionBlocks + is non-zero. If not specified, is assumed to be "0 - N/A". + :ivar qualityFlags: List of codes indicating the quality of the + reading, using specification: Bit 0 - valid: data that has gone + through all required validation checks and either passed them + all or has been verified Bit 1 - manually edited: Replaced or + approved by a human Bit 2 - estimated using reference day: data + value was replaced by a machine computed value based on analysis + of historical data using the same type of measurement. Bit 3 - + estimated using linear interpolation: data value was computed + using linear interpolation based on the readings before and + after it Bit 4 - questionable: data that has failed one or more + checks Bit 5 - derived: data that has been calculated (using + logic or mathematical operations), not necessarily measured + directly Bit 6 - projected (forecast): data that has been + calculated as a projection or forecast of future readings + :ivar timePeriod: The time interval associated with the reading. If + not specified, then defaults to the intervalLength specified in + the associated ReadingType. + :ivar touTier: Indicates the time of use tier related to the + reading. REQUIRED if ReadingType numberOfTouTiers is non-zero. + If not specified, is assumed to be "0 - N/A". + :ivar value: Value in units specified by ReadingType + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + consumptionBlock: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + qualityFlags: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 2, + "format": "base16", + }) + timePeriod: Optional[DateTimeInterval] = field(default=None, metadata={ + "type": "Element", + }) + touTier: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + value: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "min_inclusive": -140737488355328, + "max_inclusive": 140737488355328, + }) + + +@dataclass +class ReadingLink(Link): + """ + A Link to a Reading. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ReadingType(Resource): + """Type of data conveyed by a specific Reading. + + See IEC 61968 Part 9 Annex C for full definitions of these values. + + :ivar accumulationBehaviour: The “accumulation behaviour” indicates + how the value is represented to accumulate over time. + :ivar calorificValue: The amount of heat generated when a given mass + of fuel is completely burned. The CalorificValue is used to + convert the measured volume or mass of gas into kWh. The + CalorificValue attribute represents the current active value. + :ivar commodity: Indicates the commodity applicable to this + ReadingType. + :ivar conversionFactor: Accounts for changes in the volume of gas + based on temperature and pressure. The ConversionFactor + attribute represents the current active value. The + ConversionFactor is dimensionless. The default value for the + ConversionFactor is 1, which means no conversion is applied. A + price server can advertise a new/different value at any time. + :ivar dataQualifier: The data type can be used to describe a salient + attribute of the data. Possible values are average, absolute, + and etc. + :ivar flowDirection: Anything involving current might have a flow + direction. Possible values include forward and reverse. + :ivar intervalLength: Default interval length specified in seconds. + :ivar kind: Compound class that contains kindCategory and kindIndex + :ivar maxNumberOfIntervals: To be populated for mirrors of interval + data to set the expected number of intervals per ReadingSet. + Servers may discard intervals received that exceed this number. + :ivar numberOfConsumptionBlocks: Number of consumption blocks. 0 + means not applicable, and is the default if not specified. The + value needs to be at least 1 if any actual prices are provided. + :ivar numberOfTouTiers: The number of TOU tiers that can be used by + any resource configured by this ReadingType. Servers SHALL + populate this value with the largest touTier value that will + <i>ever</i> be used while this ReadingType is in + effect. Servers SHALL set numberOfTouTiers equal to the number + of standard TOU tiers plus the number of CPP tiers that may be + used while this ReadingType is in effect. Servers SHALL specify + a value between 0 and 255 (inclusive) for numberOfTouTiers + (servers providing flat rate pricing SHOULD set numberOfTouTiers + to 0, as in practice there is no difference between having no + tiers and having one tier). + :ivar phase: Contains phase information associated with the type. + :ivar powerOfTenMultiplier: Indicates the power of ten multiplier + applicable to the unit of measure of this ReadingType. + :ivar subIntervalLength: Default sub-interval length specified in + seconds for Readings of ReadingType. Some demand calculations + are done over a number of smaller intervals. For example, in a + rolling demand calculation, the demand value is defined as the + rolling sum of smaller intervals over the intervalLength. The + subintervalLength is the length of the smaller interval in this + calculation. It SHALL be an integral division of the + intervalLength. The number of sub-intervals can be calculated by + dividing the intervalLength by the subintervalLength. + :ivar supplyLimit: Reflects the supply limit set in the meter. This + value can be compared to the Reading value to understand if + limits are being approached or exceeded. Units follow the same + definition as in this ReadingType. + :ivar tieredConsumptionBlocks: Specifies whether or not the + consumption blocks are differentiated by TOUTier or not. Default + is false, if not specified. true = consumption accumulated over + individual tiers false = consumption accumulated over all tiers + :ivar uom: Indicates the measurement type for the units of measure + for the readings of this type. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + accumulationBehaviour: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + calorificValue: Optional[UnitValueType] = field(default=None, metadata={ + "type": "Element", + }) + commodity: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + conversionFactor: Optional[UnitValueType] = field(default=None, metadata={ + "type": "Element", + }) + dataQualifier: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + flowDirection: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + intervalLength: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + kind: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + maxNumberOfIntervals: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + numberOfConsumptionBlocks: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + numberOfTouTiers: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + phase: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + powerOfTenMultiplier: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + subIntervalLength: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + supplyLimit: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "max_inclusive": 281474976710655, + }) + tieredConsumptionBlocks: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + uom: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class ReadingTypeLink(Link): + """ + SHALL contain a Link to an instance of ReadingType. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class Registration(Resource): + """ + Registration represents an authorization to access the resources on a host. + + :ivar dateTimeRegistered: Contains the time at which this + registration was created, by which clients MAY prioritize + information providers with the most recent registrations, when + no additional direction from the consumer is available. + :ivar pIN: Contains the registration PIN number associated with the + device, including the checksum digit. + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + dateTimeRegistered: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + pIN: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class RegistrationLink(Link): + """ + SHALL contain a Link to an instance of Registration. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RespondableResource(Resource): + """ + A Resource to which a Response can be requested. + + :ivar replyTo: A reference to the response resource address (URI). + Required on a response to a GET if responseRequired is "true". + :ivar responseRequired: Indicates whether or not a response is + required upon receipt, creation or update of this resource. + Responses shall be posted to the collection specified in + "replyTo". If the resource has a deviceCategory field, devices + that match one or more of the device types indicated in + deviceCategory SHALL respond according to the rules listed + below. If the category does not match, the device SHALL NOT + respond. If the resource does not have a deviceCategory field, a + device receiving the resource SHALL respond according to the + rules listed below. Value encoded as hex according to the + following bit assignments, any combination is possible. See + Table 27 for the list of appropriate Response status codes to be + sent for these purposes. 0 - End device shall indicate that + message was received 1 - End device shall indicate specific + response. 2 - End user / customer response is required. All + other values reserved. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + replyTo: Optional[str] = field(default=None, metadata={ + "type": "Attribute", + }) + responseRequired: bytes = field(default=b"\x00", + metadata={ + "type": "Attribute", + "max_length": 1, + "format": "base16", + }) + + +@dataclass +class Response(Resource): + """ + The Response object is the generic response data repository which is + extended for specific function sets. + + :ivar createdDateTime: The createdDateTime field contains the date + and time when the acknowledgement/status occurred in the client. + The client will provide the timestamp to ensure the proper time + is captured in case the response is delayed in reaching the + server (server receipt time would not be the same as the actual + confirmation time). The time reported from the client should be + relative to the time server indicated by the + FunctionSetAssignment that also indicated the event resource; if + no FunctionSetAssignment exists, the time of the server where + the event resource was hosted. + :ivar endDeviceLFDI: Contains the LFDI of the device providing the + response. + :ivar status: The status field contains the acknowledgement or + status. Each event type (DRLC, DER, Price, or Text) can return + different status information (e.g. an Acknowledge will be + returned for a Price event where a DRLC event can return Event + Received, Event Started, and Event Completed). The Status field + value definitions are defined in Table 27: Response Types by + Function Set. + :ivar subject: The subject field provides a method to match the + response with the originating event. It is populated with the + mRID of the original object. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + createdDateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + endDeviceLFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 20, + "format": "base16", + }) + status: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + subject: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + + +@dataclass +class SelfDeviceLink(Link): + """ + SHALL contain a Link to an instance of SelfDevice. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ServiceSupplierLink(Link): + """ + SHALL contain a Link to an instance of ServiceSupplier. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class SubscribableResource(Resource): + """ + A Resource to which a Subscription can be requested. + + :ivar subscribable: Indicates whether or not subscriptions are + supported for this resource, and whether or not conditional + (thresholds) are supported. If not specified, is "not + subscribable" (0). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + subscribable: int = field(default=0, metadata={ + "type": "Attribute", + }) + + +@dataclass +class SubscriptionBase(Resource): + """Holds the information related to a client subscription to receive + updates to a resource automatically. + + The actual resources may be passed in the Notification by specifying + a specific xsi:type for the Resource and passing the full + representation. + + :ivar subscribedResource: The resource for which the subscription + applies. Query string parameters SHALL NOT be specified when + subscribing to list resources. Should a query string parameter + be specified, servers SHALL ignore them. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + subscribedResource: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class SupplyInterruptionOverride(Resource): + """SupplyInterruptionOverride: There may be periods of time when social, regulatory or other concerns mean that service should not be interrupted, even when available credit has been exhausted. Each Prepayment instance links to a List of SupplyInterruptionOverride instances. Each SupplyInterruptionOverride defines a contiguous period of time during which supply SHALL NOT be interrupted. + + :ivar description: The description is a human readable text + describing or naming the object. + :ivar interval: Interval defines the period of time during which + supply should not be interrupted. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + interval: Optional[DateTimeInterval] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class SupportedLocale(Resource): + """ + Specifies a locale that is supported. + + :ivar locale: The code for a locale that is supported + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + locale: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 42, + }) + + +@dataclass +class TariffProfileLink(Link): + """ + SHALL contain a Link to an instance of TariffProfile. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class Time(Resource): + """ + Contains the representation of time, constantly updated. + + :ivar currentTime: The current time, in the format defined by + TimeType. + :ivar dstEndTime: Time at which daylight savings ends (dstOffset no + longer applied). Result of dstEndRule calculation. + :ivar dstOffset: Daylight savings time offset from local standard + time. A typical practice is advancing clocks one hour when + daylight savings time is in effect, which would result in a + positive dstOffset. + :ivar dstStartTime: Time at which daylight savings begins (apply + dstOffset). Result of dstStartRule calculation. + :ivar localTime: Local time: localTime = currentTime + tzOffset (+ + dstOffset when in effect). + :ivar quality: Metric indicating the quality of the time source from + which the service acquired time. Lower (smaller) quality + enumeration values are assumed to be more accurate. 3 - time + obtained from external authoritative source such as NTP 4 - time + obtained from level 3 source 5 - time manually set or obtained + from level 4 source 6 - time obtained from level 5 source 7 - + time intentionally uncoordinated All other values are reserved + for future use. + :ivar tzOffset: Local time zone offset from currentTime. Does not + include any daylight savings time offsets. For American time + zones, a negative tzOffset SHALL be used (eg, EST = GMT-5 which + is -18000). + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + currentTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + dstEndTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + dstOffset: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + dstStartTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + localTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + quality: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + tzOffset: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class TimeLink(Link): + """ + SHALL contain a Link to an instance of Time. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class UsagePointLink(Link): + """ + SHALL contain a Link to an instance of UsagePoint. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class AccountBalance(Resource): + """AccountBalance contains the regular credit and emergency credit balance + for this given service or commodity prepay instance. + + It may also contain status information concerning the balance data. + + :ivar availableCredit: AvailableCredit shows the balance of the sum + of credits minus the sum of charges. In a Central Wallet mode + this value may be passed down to the Prepayment server via an + out-of-band mechanism. In Local or ESI modes, this value may be + calculated based upon summation of CreditRegister transactions + minus consumption charges calculated using Metering (and + possibly Pricing) function set data. This value may be negative; + for instance, if disconnection is prevented due to a Supply + Interruption Override. + :ivar creditStatus: CreditStatus identifies whether the present + value of availableCredit is considered OK, low, exhausted, or + negative. + :ivar emergencyCredit: EmergencyCredit is the amount of credit still + available for the given service or commodity prepayment + instance. If both availableCredit and emergyCredit are + exhausted, then service will typically be disconnected. + :ivar emergencyCreditStatus: EmergencyCreditStatus identifies + whether the present value of emergencyCredit is considered OK, + low, exhausted, or negative. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + availableCredit: Optional[AccountingUnit] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + creditStatus: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + emergencyCredit: Optional[AccountingUnit] = field(default=None, metadata={ + "type": "Element", + }) + emergencyCreditStatus: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class ActiveBillingPeriodListLink(ListLink): + """ + SHALL contain a Link to a List of active BillingPeriod instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveCreditRegisterListLink(ListLink): + """ + SHALL contain a Link to a List of active CreditRegister instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveDERControlListLink(ListLink): + """ + SHALL contain a Link to a List of active DERControl instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveEndDeviceControlListLink(ListLink): + """ + SHALL contain a Link to a List of active EndDeviceControl instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveFlowReservationListLink(ListLink): + """ + SHALL contain a Link to a List of active FlowReservation instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveProjectionReadingListLink(ListLink): + """ + SHALL contain a Link to a List of active ProjectionReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveSupplyInterruptionOverrideListLink(ListLink): + """ + SHALL contain a Link to a List of active SupplyInterruptionOverride + instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveTargetReadingListLink(ListLink): + """ + SHALL contain a Link to a List of active TargetReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveTextMessageListLink(ListLink): + """ + SHALL contain a Link to a List of active TextMessage instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ActiveTimeTariffIntervalListLink(ListLink): + """ + SHALL contain a Link to a List of active TimeTariffInterval instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class AssociatedDERProgramListLink(ListLink): + """ + SHALL contain a Link to a List of DERPrograms having the DERControl(s) for + this DER. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class BillingPeriodListLink(ListLink): + """ + SHALL contain a Link to a List of BillingPeriod instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class BillingReading(ReadingBase): + """Data captured at regular intervals of time. + + Interval data could be captured as incremental data, absolute data, + or relative data. The source for the data is usually a tariff + quantity or an engineering quantity. Data is typically captured in + time-tagged, uniform, fixed-length intervals of 5 min, 10 min, 15 + min, 30 min, or 60 min. However, consumption aggregations can also + be represented with this class. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Charge: List[Charge] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class BillingReadingListLink(ListLink): + """ + SHALL contain a Link to a List of BillingReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class BillingReadingSetListLink(ListLink): + """ + SHALL contain a Link to a List of BillingReadingSet instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ConsumptionTariffIntervalList(List_type): + """ + A List element to hold ConsumptionTariffInterval objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ConsumptionTariffInterval: List[ConsumptionTariffInterval] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class ConsumptionTariffIntervalListLink(ListLink): + """ + SHALL contain a Link to a List of ConsumptionTariffInterval instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class CreditRegister(IdentifiedObject): + """CreditRegister instances define a credit-modifying transaction. + + Typically this would be a credit-adding transaction, but may be a + subtracting transaction (perhaps in response to an out-of-band debt + signal). + + :ivar creditAmount: CreditAmount is the amount of credit being added + by a particular CreditRegister transaction. Negative values + indicate that credit is being subtracted. + :ivar creditType: CreditType indicates whether the credit + transaction applies to regular or emergency credit. + :ivar effectiveTime: EffectiveTime identifies the time at which the + credit transaction goes into effect. For credit addition + transactions, this is typically the moment at which the + transaction takes place. For credit subtraction transactions, + (e.g., non-fuel debt recovery transactions initiated from a + back-haul or ESI) this may be a future time at which credit is + deducted. + :ivar token: Token is security data that authenticates the + legitimacy of the transaction. The details of this token are not + defined by IEEE 2030.5. How a Prepayment server handles this + field is left as vendor specific implementation or will be + defined by one or more other standards. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + creditAmount: Optional[AccountingUnit] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + creditType: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + effectiveTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + token: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + + +@dataclass +class CreditRegisterListLink(ListLink): + """ + SHALL contain a Link to a List of CreditRegister instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class CustomerAccountListLink(ListLink): + """ + SHALL contain a Link to a List of CustomerAccount instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class CustomerAgreementListLink(ListLink): + """ + SHALL contain a Link to a List of CustomerAgreement instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERAvailability(SubscribableResource): + """ + Indicates current reserve generation status. + + :ivar availabilityDuration: Indicates number of seconds the DER will + be able to deliver active power at the reservePercent level. + :ivar maxChargeDuration: Indicates number of seconds the DER will be + able to receive active power at the reserveChargePercent level. + :ivar readingTime: The timestamp when the DER availability was last + updated. + :ivar reserveChargePercent: Percent of continuous received active + power (%setMaxChargeRateW) that is estimated to be available in + reserve. + :ivar reservePercent: Percent of continuous delivered active power + (%setMaxW) that is estimated to be available in reserve. + :ivar statVarAvail: Estimated reserve reactive power, in var. + Represents the lesser of received or delivered reactive power. + :ivar statWAvail: Estimated reserve active power, in watts. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + availabilityDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + maxChargeDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + readingTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + reserveChargePercent: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + reservePercent: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + statVarAvail: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + statWAvail: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class DERControlBase: + """ + Distributed Energy Resource (DER) control values. + + :ivar opModConnect: Set DER as connected (true) or disconnected + (false). Used in conjunction with ramp rate when re-connecting. + Implies galvanic isolation. + :ivar opModEnergize: Set DER as energized (true) or de-energized + (false). Used in conjunction with ramp rate when re-energizing. + :ivar opModFixedPFAbsorbW: The opModFixedPFAbsorbW function + specifies a requested fixed Power Factor (PF) setting for when + active power is being absorbed. The actual displacement SHALL be + within the limits established by setMinPFOverExcited and + setMinPFUnderExcited. If issued simultaneously with other + reactive power controls (e.g. opModFixedVar) the control + resulting in least var magnitude SHOULD take precedence. + :ivar opModFixedPFInjectW: The opModFixedPFInjectW function + specifies a requested fixed Power Factor (PF) setting for when + active power is being injected. The actual displacement SHALL be + within the limits established by setMinPFOverExcited and + setMinPFUnderExcited. If issued simultaneously with other + reactive power controls (e.g. opModFixedVar) the control + resulting in least var magnitude SHOULD take precedence. + :ivar opModFixedVar: The opModFixedVar function specifies the + delivered or received reactive power setpoint. The context for + the setpoint value is determined by refType and SHALL be one of + %setMaxW, %setMaxVar, or %statVarAvail. If issued + simultaneously with other reactive power controls (e.g. + opModFixedPFInjectW) the control resulting in least var + magnitude SHOULD take precedence. + :ivar opModFixedW: The opModFixedW function specifies a requested + charge or discharge mode setpoint, in %setMaxChargeRateW if + negative value or %setMaxW or %setMaxDischargeRateW if positive + value (in hundredths). + :ivar opModFreqDroop: Specifies a frequency-watt operation. This + operation limits active power generation or consumption when the + line frequency deviates from nominal by a specified amount. + :ivar opModFreqWatt: Specify DERCurveLink for curveType == 0. The + Frequency-Watt function limits active power generation or + consumption when the line frequency deviates from nominal by a + specified amount. The Frequency-Watt curve is specified as an + array of Frequency-Watt pairs that are interpolated into a + piecewise linear function with hysteresis. The x value of each + pair specifies a frequency in Hz. The y value specifies a + corresponding active power output in %setMaxW. + :ivar opModHFRTMayTrip: Specify DERCurveLink for curveType == 1. The + High Frequency Ride-Through (HFRT) function is specified by one + or two duration-frequency curves that define the operating + region under high frequency conditions. Each HFRT curve is + specified by an array of duration-frequency pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given frequency in seconds). The y value of each pair + specifies a frequency, in Hz. This control specifies the "may + trip" region. + :ivar opModHFRTMustTrip: Specify DERCurveLink for curveType == 2. + The High Frequency Ride-Through (HFRT) function is specified by + a duration-frequency curve that defines the operating region + under high frequency conditions. Each HFRT curve is specified + by an array of duration-frequency pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given frequency in seconds). The y value of each pair + specifies a frequency, in Hz. This control specifies the "must + trip" region. + :ivar opModHVRTMayTrip: Specify DERCurveLink for curveType == 3. The + High Voltage Ride-Through (HVRT) function is specified by one, + two, or three duration-volt curves that define the operating + region under high voltage conditions. Each HVRT curve is + specified by an array of duration-volt pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given voltage in seconds). The y value of each pair + specifies an effective percentage voltage, defined as ((locally + measured voltage - setVRefOfs / setVRef). This control specifies + the "may trip" region. + :ivar opModHVRTMomentaryCessation: Specify DERCurveLink for + curveType == 4. The High Voltage Ride-Through (HVRT) function + is specified by duration-volt curves that define the operating + region under high voltage conditions. Each HVRT curve is + specified by an array of duration-volt pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given voltage in seconds). The y value of each pair + specifies an effective percent voltage, defined as ((locally + measured voltage - setVRefOfs) / setVRef). This control + specifies the "momentary cessation" region. + :ivar opModHVRTMustTrip: Specify DERCurveLink for curveType == 5. + The High Voltage Ride-Through (HVRT) function is specified by + duration-volt curves that define the operating region under high + voltage conditions. Each HVRT curve is specified by an array of + duration-volt pairs that will be interpolated into a piecewise + linear function that defines an operating region. The x value + of each pair specifies a duration (time at a given voltage in + seconds). The y value of each pair specifies an effective + percent voltage, defined as ((locally measured voltage - + setVRefOfs) / setVRef). This control specifies the "must trip" + region. + :ivar opModLFRTMayTrip: Specify DERCurveLink for curveType == 6. The + Low Frequency Ride-Through (LFRT) function is specified by one + or two duration-frequency curves that define the operating + region under low frequency conditions. Each LFRT curve is + specified by an array of duration-frequency pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given frequency in seconds). The y value of each pair + specifies a frequency, in Hz. This control specifies the "may + trip" region. + :ivar opModLFRTMustTrip: Specify DERCurveLink for curveType == 7. + The Low Frequency Ride-Through (LFRT) function is specified by a + duration-frequency curve that defines the operating region under + low frequency conditions. Each LFRT curve is specified by an + array of duration-frequency pairs that will be interpolated into + a piecewise linear function that defines an operating region. + The x value of each pair specifies a duration (time at a given + frequency in seconds). The y value of each pair specifies a + frequency, in Hz. This control specifies the "must trip" region. + :ivar opModLVRTMayTrip: Specify DERCurveLink for curveType == 8. The + Low Voltage Ride-Through (LVRT) function is specified by one, + two, or three duration-volt curves that define the operating + region under low voltage conditions. Each LVRT curve is + specified by an array of duration-volt pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given voltage in seconds). The y value of each pair + specifies an effective percent voltage, defined as ((locally + measured voltage - setVRefOfs) / setVRef). This control + specifies the "may trip" region. + :ivar opModLVRTMomentaryCessation: Specify DERCurveLink for + curveType == 9. The Low Voltage Ride-Through (LVRT) function is + specified by duration-volt curves that define the operating + region under low voltage conditions. Each LVRT curve is + specified by an array of duration-volt pairs that will be + interpolated into a piecewise linear function that defines an + operating region. The x value of each pair specifies a duration + (time at a given voltage in seconds). The y value of each pair + specifies an effective percent voltage, defined as ((locally + measured voltage - setVRefOfs) / setVRef). This control + specifies the "momentary cessation" region. + :ivar opModLVRTMustTrip: Specify DERCurveLink for curveType == 10. + The Low Voltage Ride-Through (LVRT) function is specified by + duration-volt curves that define the operating region under low + voltage conditions. Each LVRT curve is specified by an array of + duration-volt pairs that will be interpolated into a piecewise + linear function that defines an operating region. The x value + of each pair specifies a duration (time at a given voltage in + seconds). The y value of each pair specifies an effective + percent voltage, defined as ((locally measured voltage - + setVRefOfs) / setVRef). This control specifies the "must trip" + region. + :ivar opModMaxLimW: The opModMaxLimW function sets the maximum + active power generation level at the electrical coupling point + as a percentage of set capacity (%setMaxW, in hundredths). This + limitation may be met e.g. by reducing PV output or by using + excess PV output to charge associated storage. + :ivar opModTargetVar: Target reactive power, in var. This control is + likely to be more useful for aggregators, as individual DERs may + not be able to maintain a target setting. + :ivar opModTargetW: Target output power, in Watts. This control is + likely to be more useful for aggregators, as individual DERs may + not be able to maintain a target setting. + :ivar opModVoltVar: Specify DERCurveLink for curveType == 11. The + static volt-var function provides over- or under-excited var + compensation as a function of measured voltage. The volt-var + curve is specified as an array of volt-var pairs that are + interpolated into a piecewise linear function with hysteresis. + The x value of each pair specifies an effective percent voltage, + defined as ((locally measured voltage - setVRefOfs) / setVRef) + and SHOULD support a domain of at least 0 - 135. If VRef is + present in DERCurve, then the x value of each pair is + additionally multiplied by (VRef / 10000). The y value specifies + a target var output interpreted as a signed percentage (-100 to + 100). The meaning of the y value is determined by yRefType and + must be one of %setMaxW, %setMaxVar, or %statVarAvail. + :ivar opModVoltWatt: Specify DERCurveLink for curveType == 12. The + Volt-Watt reduces active power output as a function of measured + voltage. The Volt-Watt curve is specified as an array of Volt- + Watt pairs that are interpolated into a piecewise linear + function with hysteresis. The x value of each pair specifies an + effective percent voltage, defined as ((locally measured voltage + - setVRefOfs) / setVRef) and SHOULD support a domain of at least + 0 - 135. The y value specifies an active power output + interpreted as a percentage (0 - 100). The meaning of the y + value is determined by yRefType and must be one of %setMaxW or + %statWAvail. + :ivar opModWattPF: Specify DERCurveLink for curveType == 13. The + Watt-PF function varies Power Factor (PF) as a function of + delivered active power. The Watt-PF curve is specified as an + array of Watt-PF coordinates that are interpolated into a + piecewise linear function with hysteresis. The x value of each + pair specifies a watt setting in %setMaxW, (0 - 100). The PF + output setting is a signed displacement in y value (PF sign + SHALL be interpreted according to the EEI convention, where + unity PF is considered unsigned). These settings are not + expected to be updated very often during the life of the + installation, therefore only a single curve is required. If + issued simultaneously with other reactive power controls (e.g. + opModFixedPFInjectW) the control resulting in least var + magnitude SHOULD take precedence. + :ivar opModWattVar: Specify DERCurveLink for curveType == 14. The + Watt-Var function varies vars as a function of delivered active + power. The Watt-Var curve is specified as an array of Watt-Var + pairs that are interpolated into a piecewise linear function + with hysteresis. The x value of each pair specifies a watt + setting in %setMaxW, (0-100). The y value specifies a target var + output interpreted as a signed percentage (-100 to 100). The + meaning of the y value is determined by yRefType and must be one + of %setMaxW, %setMaxVar, or %statVarAvail. + :ivar rampTms: Requested ramp time, in hundredths of a second, for + the device to transition from the current DERControl mode + setting(s) to the new mode setting(s). If absent, use default + ramp rate (setGradW). Resolution is 1/100 sec. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + opModConnect: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + opModEnergize: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + opModFixedPFAbsorbW: Optional[PowerFactorWithExcitation] = field(default=None, + metadata={ + "type": "Element", + }) + opModFixedPFInjectW: Optional[PowerFactorWithExcitation] = field(default=None, + metadata={ + "type": "Element", + }) + opModFixedVar: Optional[FixedVar] = field(default=None, metadata={ + "type": "Element", + }) + opModFixedW: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + opModFreqDroop: Optional[FreqDroopType] = field(default=None, metadata={ + "type": "Element", + }) + opModFreqWatt: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModHFRTMayTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModHFRTMustTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModHVRTMayTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModHVRTMomentaryCessation: Optional[DERCurveLink] = field(default=None, + metadata={ + "type": "Element", + }) + opModHVRTMustTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModLFRTMayTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModLFRTMustTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModLVRTMayTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModLVRTMomentaryCessation: Optional[DERCurveLink] = field(default=None, + metadata={ + "type": "Element", + }) + opModLVRTMustTrip: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModMaxLimW: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + opModTargetVar: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + opModTargetW: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + opModVoltVar: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModVoltWatt: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModWattPF: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + opModWattVar: Optional[DERCurveLink] = field(default=None, metadata={ + "type": "Element", + }) + rampTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class DERControlListLink(ListLink): + """ + SHALL contain a Link to a List of DERControl instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERControlResponse(Response): + """ + A response to a DERControl. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERCurve(IdentifiedObject): + """DER related curves such as Volt-Var mode curves. + + Relationship between an independent variable (X-axis) and a + dependent variable (Y-axis). + + :ivar autonomousVRefEnable: If the curveType is opModVoltVar, then + this field MAY be present. If the curveType is not opModVoltVar, + then this field SHALL NOT be present. Enable/disable autonomous + vRef adjustment. When enabled, the Volt-Var curve characteristic + SHALL be adjusted autonomously as vRef changes and + autonomousVRefTimeConstant SHALL be present. If a DER is able to + support Volt-Var mode but is unable to support autonomous vRef + adjustment, then the DER SHALL execute the curve without + autonomous vRef adjustment. If not specified, then the value is + false. + :ivar autonomousVRefTimeConstant: If the curveType is opModVoltVar, + then this field MAY be present. If the curveType is not + opModVoltVar, then this field SHALL NOT be present. Adjustment + range for vRef time constant, in hundredths of a second. + :ivar creationTime: The time at which the object was created. + :ivar CurveData: + :ivar curveType: Specifies the associated curve-based control mode. + :ivar openLoopTms: Open loop response time, the time to ramp up to + 90% of the new target in response to the change in voltage, in + hundredths of a second. Resolution is 1/100 sec. A value of 0 is + used to mean no limit. When not present, the device SHOULD + follow its default behavior. + :ivar rampDecTms: Decreasing ramp rate, interpreted as a percentage + change in output capability limit per second (e.g. %setMaxW / + sec). Resolution is in hundredths of a percent/second. A value + of 0 means there is no limit. If absent, ramp rate defaults to + setGradW. + :ivar rampIncTms: Increasing ramp rate, interpreted as a percentage + change in output capability limit per second (e.g. %setMaxW / + sec). Resolution is in hundredths of a percent/second. A value + of 0 means there is no limit. If absent, ramp rate defaults to + rampDecTms. + :ivar rampPT1Tms: The configuration parameter for a low-pass filter, + PT1 is a time, in hundredths of a second, in which the filter + will settle to 95% of a step change in the input value. + Resolution is 1/100 sec. + :ivar vRef: If the curveType is opModVoltVar, then this field MAY be + present. If the curveType is not opModVoltVar, then this field + SHALL NOT be present. The nominal AC voltage (RMS) adjustment to + the voltage curve points for Volt-Var curves. + :ivar xMultiplier: Exponent for X-axis value. + :ivar yMultiplier: Exponent for Y-axis value. + :ivar yRefType: The Y-axis units context. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + autonomousVRefEnable: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + autonomousVRefTimeConstant: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + creationTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + CurveData: List[CurveData] = field(default_factory=list, + metadata={ + "type": "Element", + "min_occurs": 1, + "max_occurs": 10, + }) + curveType: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + openLoopTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + rampDecTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + rampIncTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + rampPT1Tms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + vRef: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + xMultiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + yMultiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + yRefType: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DERCurveListLink(ListLink): + """ + SHALL contain a Link to a List of DERCurve instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERListLink(ListLink): + """ + SHALL contain a Link to a List of DER instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERProgramListLink(ListLink): + """ + SHALL contain a Link to a List of DERProgram instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DERSettings(SubscribableResource): + """ + Distributed energy resource settings. + + :ivar modesEnabled: Bitmap indicating the DER Controls enabled on + the device. See DERControlType for values. If a control is + supported (see DERCapability::modesSupported), but not enabled, + the control will not be executed if encountered. + :ivar setESDelay: Enter service delay, in hundredths of a second. + :ivar setESHighFreq: Enter service frequency high. Specified in + hundredths of Hz. + :ivar setESHighVolt: Enter service voltage high. Specified as an + effective percent voltage, defined as (100% * (locally measured + voltage - setVRefOfs) / setVRef), in hundredths of a percent. + :ivar setESLowFreq: Enter service frequency low. Specified in + hundredths of Hz. + :ivar setESLowVolt: Enter service voltage low. Specified as an + effective percent voltage, defined as (100% * (locally measured + voltage - setVRefOfs) / setVRef), in hundredths of a percent. + :ivar setESRampTms: Enter service ramp time, in hundredths of a + second. + :ivar setESRandomDelay: Enter service randomized delay, in + hundredths of a second. + :ivar setGradW: Set default rate of change (ramp rate) of active + power output due to command or internal action, defined in + %setWMax / second. Resolution is in hundredths of a + percent/second. A value of 0 means there is no limit. + Interpreted as a percentage change in output capability limit + per second when used as a default ramp rate. + :ivar setMaxA: AC current maximum. Maximum AC current in RMS + Amperes. + :ivar setMaxAh: Maximum usable energy storage capacity of the DER, + in AmpHours. Note: this may be different from physical + capability. + :ivar setMaxChargeRateVA: Apparent power charge maximum. Maximum + apparent power the DER can absorb from the grid in Volt-Amperes. + May differ from the apparent power maximum (setMaxVA). + :ivar setMaxChargeRateW: Maximum rate of energy transfer received by + the storage device, in Watts. Defaults to rtgMaxChargeRateW. + :ivar setMaxDischargeRateVA: Apparent power discharge maximum. + Maximum apparent power the DER can deliver to the grid in Volt- + Amperes. May differ from the apparent power maximum (setMaxVA). + :ivar setMaxDischargeRateW: Maximum rate of energy transfer + delivered by the storage device, in Watts. Defaults to + rtgMaxDischargeRateW. + :ivar setMaxV: AC voltage maximum setting. + :ivar setMaxVA: Set limit for maximum apparent power capability of + the DER (in VA). Defaults to rtgMaxVA. + :ivar setMaxVar: Set limit for maximum reactive power delivered by + the DER (in var). SHALL be a positive value &lt;= rtgMaxVar + (default). + :ivar setMaxVarNeg: Set limit for maximum reactive power received by + the DER (in var). If present, SHALL be a negative value + &gt;= rtgMaxVarNeg (default). If absent, defaults to + negative setMaxVar. + :ivar setMaxW: Set limit for maximum active power capability of the + DER (in W). Defaults to rtgMaxW. + :ivar setMaxWh: Maximum energy storage capacity of the DER, in + WattHours. Note: this may be different from physical capability. + :ivar setMinPFOverExcited: Set minimum Power Factor displacement + limit of the DER when injecting reactive power (over-excited); + SHALL be a positive value between 0.0 (typically &gt; 0.7) + and 1.0. SHALL be &gt;= rtgMinPFOverExcited (default). + :ivar setMinPFUnderExcited: Set minimum Power Factor displacement + limit of the DER when absorbing reactive power (under-excited); + SHALL be a positive value between 0.0 (typically &gt; 0.7) + and 0.9999. If present, SHALL be &gt;= rtgMinPFUnderExcited + (default). If absent, defaults to setMinPFOverExcited. + :ivar setMinV: AC voltage minimum setting. + :ivar setSoftGradW: Set soft-start rate of change (soft-start ramp + rate) of active power output due to command or internal action, + defined in %setWMax / second. Resolution is in hundredths of a + percent/second. A value of 0 means there is no limit. + Interpreted as a percentage change in output capability limit + per second when used as a ramp rate. + :ivar setVNom: AC voltage nominal setting. + :ivar setVRef: The nominal AC voltage (RMS) at the utility's point + of common coupling. + :ivar setVRefOfs: The nominal AC voltage (RMS) offset between the + DER's electrical connection point and the utility's point of + common coupling. + :ivar updatedTime: Specifies the time at which the DER information + was last updated. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + modesEnabled: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 4, + "format": "base16", + }) + setESDelay: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESHighFreq: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESHighVolt: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESLowFreq: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESLowVolt: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESRampTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESRandomDelay: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setGradW: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + setMaxA: Optional[CurrentRMS] = field(default=None, metadata={ + "type": "Element", + }) + setMaxAh: Optional[AmpereHour] = field(default=None, metadata={ + "type": "Element", + }) + setMaxChargeRateVA: Optional[ApparentPower] = field(default=None, + metadata={ + "type": "Element", + }) + setMaxChargeRateW: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + setMaxDischargeRateVA: Optional[ApparentPower] = field(default=None, + metadata={ + "type": "Element", + }) + setMaxDischargeRateW: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + }) + setMaxV: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + setMaxVA: Optional[ApparentPower] = field(default=None, metadata={ + "type": "Element", + }) + setMaxVar: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + setMaxVarNeg: Optional[ReactivePower] = field(default=None, metadata={ + "type": "Element", + }) + setMaxW: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + setMaxWh: Optional[WattHour] = field(default=None, metadata={ + "type": "Element", + }) + setMinPFOverExcited: Optional[PowerFactor] = field(default=None, + metadata={ + "type": "Element", + }) + setMinPFUnderExcited: Optional[PowerFactor] = field(default=None, + metadata={ + "type": "Element", + }) + setMinV: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + setSoftGradW: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setVNom: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + setVRef: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + setVRefOfs: Optional[VoltageRMS] = field(default=None, metadata={ + "type": "Element", + }) + updatedTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DERStatus(SubscribableResource): + """ + DER status information. + + :ivar alarmStatus: Bitmap indicating the status of DER alarms (see + DER LogEvents for more details). 0 - DER_FAULT_OVER_CURRENT 1 - + DER_FAULT_OVER_VOLTAGE 2 - DER_FAULT_UNDER_VOLTAGE 3 - + DER_FAULT_OVER_FREQUENCY 4 - DER_FAULT_UNDER_FREQUENCY 5 - + DER_FAULT_VOLTAGE_IMBALANCE 6 - DER_FAULT_CURRENT_IMBALANCE 7 - + DER_FAULT_EMERGENCY_LOCAL 8 - DER_FAULT_EMERGENCY_REMOTE 9 - + DER_FAULT_LOW_POWER_INPUT 10 - DER_FAULT_PHASE_ROTATION 11-31 - + Reserved + :ivar genConnectStatus: Connect/status value for generator DER. See + ConnectStatusType for values. + :ivar inverterStatus: DER InverterStatus/value. See + InverterStatusType for values. + :ivar localControlModeStatus: The local control mode status. See + LocalControlModeStatusType for values. + :ivar manufacturerStatus: Manufacturer status code. + :ivar operationalModeStatus: Operational mode currently in use. See + OperationalModeStatusType for values. + :ivar readingTime: The timestamp when the current status was last + updated. + :ivar stateOfChargeStatus: State of charge status. See + StateOfChargeStatusType for values. + :ivar storageModeStatus: Storage mode status. See + StorageModeStatusType for values. + :ivar storConnectStatus: Connect/status value for storage DER. See + ConnectStatusType for values. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + alarmStatus: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 4, + "format": "base16", + }) + genConnectStatus: Optional[ConnectStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + inverterStatus: Optional[InverterStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + localControlModeStatus: Optional[LocalControlModeStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + manufacturerStatus: Optional[ManufacturerStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + operationalModeStatus: Optional[OperationalModeStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + readingTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + stateOfChargeStatus: Optional[StateOfChargeStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + storageModeStatus: Optional[StorageModeStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + storConnectStatus: Optional[ConnectStatusType] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class DemandResponseProgramListLink(ListLink): + """ + SHALL contain a Link to a List of DemandResponseProgram instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class DeviceStatus(Resource): + """ + Status of device. + + :ivar changedTime: The time at which the reported values were + recorded. + :ivar onCount: The number of times that the device has been turned + on: Count of "device on" times, since the last time the counter + was reset + :ivar opState: Device operational state: 0 - Not applicable / + Unknown 1 - Not operating 2 - Operating 3 - Starting up 4 - + Shutting down 5 - At disconnect level 6 - kW ramping 7 - kVar + ramping + :ivar opTime: Total time device has operated: re-settable: + Accumulated time in seconds since the last time the counter was + reset. + :ivar Temperature: + :ivar TimeLink: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + changedTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + onCount: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + opState: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + opTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + Temperature: List[Temperature] = field(default_factory=list, metadata={ + "type": "Element", + }) + TimeLink: Optional[TimeLink] = field(default=None, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class DrResponse(Response): + """ + A response to a Demand Response Load Control (EndDeviceControl) message. + + :ivar ApplianceLoadReduction: + :ivar AppliedTargetReduction: + :ivar DutyCycle: + :ivar Offset: + :ivar overrideDuration: Indicates the amount of time, in seconds, + that the client partially opts-out during the demand response + event. When overriding within the allowed override duration, the + client SHALL send a partial opt-out (Response status code 8) for + partial opt-out upon completion, with the total time the event + was overridden (this attribute) populated. The client SHALL send + a no participation status response (status type 10) if the user + partially opts-out for longer than + EndDeviceControl.overrideDuration. + :ivar SetPoint: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ApplianceLoadReduction: Optional[ApplianceLoadReduction] = field(default=None, + metadata={ + "type": "Element", + }) + AppliedTargetReduction: Optional[AppliedTargetReduction] = field(default=None, + metadata={ + "type": "Element", + }) + DutyCycle: Optional[DutyCycle] = field(default=None, metadata={ + "type": "Element", + }) + Offset: Optional[Offset] = field(default=None, metadata={ + "type": "Element", + }) + overrideDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + SetPoint: Optional[SetPoint] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class EndDeviceControlListLink(ListLink): + """ + SHALL contain a Link to a List of EndDeviceControl instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class EndDeviceListLink(ListLink): + """ + SHALL contain a Link to a List of EndDevice instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FileList(List_type): + """ + A List element to hold File objects. + + :ivar File: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + File: List[File] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class FileListLink(ListLink): + """ + SHALL contain a Link to a List of File instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FileStatus(Resource): + """ + This object provides status of device file load and activation operations. + + :ivar activateTime: Date/time at which this File, referred to by + FileLink, will be activated. Omission of or presence and value + of this element MUST exactly match omission or presence and + value of the activateTime element from the File resource. + :ivar FileLink: + :ivar loadPercent: This element MUST be set to the percentage of the + file, indicated by FileLink, that was loaded during the latest + load attempt. This value MUST be reset to 0 each time a load + attempt is started for the File indicated by FileLink. This + value MUST be increased when an LD receives HTTP response + containing file content. This value MUST be set to 100 when the + full content of the file has been received by the LD + :ivar nextRequestAttempt: This element MUST be set to the time at + which the LD will issue its next GET request for file content + from the File indicated by FileLink + :ivar request503Count: This value MUST be reset to 0 when FileLink + is first pointed at a new File. This value MUST be incremented + each time an LD receives a 503 error from the FS. + :ivar requestFailCount: This value MUST be reset to 0 when FileLink + is first pointed at a new File. This value MUST be incremented + each time a GET request for file content failed. 503 errors MUST + be excluded from this counter. + :ivar status: Current loading status of the file indicated by + FileLink. This element MUST be set to one of the following + values: 0 - No load operation in progress 1 - File load in + progress (first request for file content has been issued by LD) + 2 - File load failed 3 - File loaded successfully (full content + of file has been received by the LD), signature verification in + progress 4 - File signature verification failed 5 - File + signature verified, waiting to activate file. 6 - File + activation failed 7 - File activation in progress 8 - File + activated successfully (this state may not be reached/persisted + through an image activation) 9-255 - Reserved for future use. + :ivar statusTime: This element MUST be set to the time at which file + status transitioned to the value indicated in the status + element. + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + activateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + FileLink: Optional[FileLink] = field(default=None, metadata={ + "type": "Element", + }) + loadPercent: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + nextRequestAttempt: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + request503Count: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + requestFailCount: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + status: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + statusTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class FlowReservationRequest(IdentifiedObject): + """Used to request flow transactions. + + Client EndDevices submit a request for charging or discharging from + the server. The server creates an associated FlowReservationResponse + containing the charging parameters and interval to provide a lower + aggregated demand at the premises, or within a larger part of the + distribution system. + + :ivar creationTime: The time at which the request was created. + :ivar durationRequested: A value that is calculated by the storage + device that defines the minimum duration, in seconds, that it + will take to complete the actual flow transaction, including any + ramp times and conditioning times, if applicable. + :ivar energyRequested: Indicates the total amount of energy, in + Watt-Hours, requested to be transferred between the storage + device and the electric power system. Positive values indicate + charging and negative values indicate discharging. This sign + convention is different than for the DER function where + discharging is positive. Note that the energyRequestNow + attribute in the PowerStatus Object must always represent a + charging solution and it is not allowed to have a negative + value. + :ivar intervalRequested: The time window during which the flow + reservation is needed. For example, if an electric vehicle is + set with a 7:00 AM time charge is needed, and price drops to the + lowest tier at 11:00 PM, then this window would likely be from + 11:00 PM until 7:00 AM. + :ivar powerRequested: Indicates the sustained level of power, in + Watts, that is requested. For charging this is calculated by the + storage device and it represents the charging system capability + (which for an electric vehicle must also account for any power + limitations due to the EVSE control pilot). For discharging, a + lower value than the inverter capability can be used as a + target. + :ivar RequestStatus: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + creationTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + durationRequested: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + energyRequested: Optional[SignedRealEnergy] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + intervalRequested: Optional[DateTimeInterval] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + powerRequested: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + RequestStatus: Optional[RequestStatus] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class FlowReservationRequestListLink(ListLink): + """ + SHALL contain a Link to a List of FlowReservationRequest instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FlowReservationResponseListLink(ListLink): + """ + SHALL contain a Link to a List of FlowReservationResponse instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FlowReservationResponseResponse(Response): + """ + A response to a FlowReservationResponse. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class FunctionSetAssignmentsListLink(ListLink): + """ + SHALL contain a Link to a List of FunctionSetAssignments instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class HistoricalReadingListLink(ListLink): + """ + SHALL contain a Link to a List of HistoricalReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class IPAddrListLink(ListLink): + """ + SHALL contain a Link to a List of IPAddr instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class IPInterfaceListLink(ListLink): + """ + SHALL contain a Link to a List of IPInterface instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class LLInterfaceListLink(ListLink): + """ + SHALL contain a Link to a List of LLInterface instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class LoadShedAvailability(Resource): + """ + Indicates current consumption status and ability to shed load. + + :ivar availabilityDuration: Indicates for how many seconds the + consuming device will be able to reduce consumption at the + maximum response level. + :ivar DemandResponseProgramLink: + :ivar sheddablePercent: Maximum percent of current operating load + that is estimated to be sheddable. + :ivar sheddablePower: Maximum amount of current operating load that + is estimated to be sheddable, in Watts. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + availabilityDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + DemandResponseProgramLink: Optional[DemandResponseProgramLink] = field(default=None, + metadata={ + "type": "Element", + }) + sheddablePercent: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + sheddablePower: Optional[ActivePower] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class LoadShedAvailabilityListLink(ListLink): + """ + SHALL contain a Link to a List of LoadShedAvailability instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class LogEventListLink(ListLink): + """ + SHALL contain a Link to a List of LogEvent instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class MessagingProgramListLink(ListLink): + """ + SHALL contain a Link to a List of MessagingProgram instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class MeterReadingBase(IdentifiedObject): + """ + A container for associating ReadingType, Readings and ReadingSets. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class MeterReadingListLink(ListLink): + """ + SHALL contain a Link to a List of MeterReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class MirrorUsagePointListLink(ListLink): + """ + SHALL contain a Link to a List of MirrorUsagePoint instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class NeighborList(List_type): + """ + List of 15.4 neighbors. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Neighbor: List[Neighbor] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class NeighborListLink(ListLink): + """ + SHALL contain a Link to a List of Neighbor instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class Notification(SubscriptionBase): + """Holds the information related to a client subscription to receive + updates to a resource automatically. + + The actual resources may be passed in the Notification by specifying + a specific xsi:type for the Resource and passing the full + representation. + + :ivar newResourceURI: The new location of the resource, if moved. + This attribute SHALL be a fully-qualified absolute URI, not a + relative reference. + :ivar Resource: + :ivar status: 0 = Default Status 1 = Subscription canceled, no + additional information 2 = Subscription canceled, resource moved + 3 = Subscription canceled, resource definition changed (e.g., a + new version of IEEE 2030.5) 4 = Subscription canceled, resource + deleted All other values reserved. + :ivar subscriptionURI: The subscription from which this notification + was triggered. This attribute SHALL be a fully-qualified + absolute URI, not a relative reference. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + newResourceURI: Optional[str] = field(default=None, metadata={ + "type": "Element", + }) + Resource: Optional[Resource] = field(default=None, metadata={ + "type": "Element", + }) + status: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + subscriptionURI: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class NotificationListLink(ListLink): + """ + SHALL contain a Link to a List of Notification instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class PowerStatus(Resource): + """ + Contains the status of the device's power sources. + + :ivar batteryStatus: Battery system status 0 = unknown 1 = normal + (more than LowChargeThreshold remaining) 2 = low (less than + LowChargeThreshold remaining) 3 = depleted (0% charge remaining) + 4 = not applicable (mains powered only) + :ivar changedTime: The time at which the reported values were + recorded. + :ivar currentPowerSource: This value will be fixed for devices + powered by a single source. This value may change for devices + able to transition between multiple power sources (mains to + battery backup, etc.). + :ivar estimatedChargeRemaining: Estimate of remaining battery charge + as a percent of full charge. + :ivar estimatedTimeRemaining: Estimated time (in seconds) to total + battery charge depletion (under current load) + :ivar PEVInfo: + :ivar sessionTimeOnBattery: If the device has a battery, this is the + time since the device last switched to battery power, or the + time since the device was restarted, whichever is less, in + seconds. + :ivar totalTimeOnBattery: If the device has a battery, this is the + total time the device has been on battery power, in seconds. It + may be reset when the battery is replaced. + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + batteryStatus: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + changedTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + currentPowerSource: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + estimatedChargeRemaining: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + estimatedTimeRemaining: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + PEVInfo: Optional[PEVInfo] = field(default=None, metadata={ + "type": "Element", + }) + sessionTimeOnBattery: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + totalTimeOnBattery: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class PrepaymentListLink(ListLink): + """ + SHALL contain a Link to a List of Prepayment instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class PriceResponse(Response): + """ + A response related to a price message. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class PriceResponseCfg(Resource): + """ + Configuration data that specifies how price responsive devices SHOULD + respond to price changes while acting upon a given RateComponent. + + :ivar consumeThreshold: Price responsive clients acting upon the + associated RateComponent SHOULD consume the associated commodity + while the price is less than this threshold. + :ivar maxReductionThreshold: Price responsive clients acting upon + the associated RateComponent SHOULD reduce consumption to the + maximum extent possible while the price is greater than this + threshold. + :ivar RateComponentLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + consumeThreshold: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + maxReductionThreshold: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + RateComponentLink: Optional[RateComponentLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class PriceResponseCfgListLink(ListLink): + """ + SHALL contain a Link to a List of PriceResponseCfg instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ProjectionReadingListLink(ListLink): + """ + SHALL contain a Link to a List of ProjectionReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RPLInstanceListLink(ListLink): + """ + SHALL contain a Link to a List of RPLInterface instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RPLSourceRoutesList(List_type): + """ + List or RPL source routes if the hosting device is the DODAGroot. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + RPLSourceRoutes: List[RPLSourceRoutes] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class RPLSourceRoutesListLink(ListLink): + """ + SHALL contain a Link to a List of RPLSourceRoutes instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RateComponentListLink(ListLink): + """ + SHALL contain a Link to a List of RateComponent instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class Reading(ReadingBase): + """ + Specific value measured by a meter or other asset. + + :ivar localID: The local identifier for this reading within the + reading set. localIDs are assigned in order of creation time. + For interval data, this value SHALL increase with each interval + time, and for block/tier readings, localID SHALL not be + specified. + :ivar subscribable: Indicates whether or not subscriptions are + supported for this resource, and whether or not conditional + (thresholds) are supported. If not specified, is "not + subscribable" (0). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + localID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 2, + "format": "base16", + }) + subscribable: int = field(default=0, metadata={ + "type": "Attribute", + }) + + +@dataclass +class ReadingListLink(ListLink): + """ + SHALL contain a Link to a List of Reading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ReadingSetBase(IdentifiedObject): + """A set of Readings of the ReadingType indicated by the parent + MeterReading. + + ReadingBase is abstract, used to define the elements common to + ReadingSet and IntervalBlock. + + :ivar timePeriod: Specifies the time range during which the + contained readings were taken. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + timePeriod: Optional[DateTimeInterval] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ReadingSetListLink(ListLink): + """ + SHALL contain a Link to a List of ReadingSet instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RespondableIdentifiedObject(RespondableResource): + """ + An IdentifiedObject to which a Response can be requested. + + :ivar mRID: The global identifier of the object. + :ivar description: The description is a human readable text + describing or naming the object. + :ivar version: Contains the version number of the object. See the + type definition for details. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + mRID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + version: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class RespondableSubscribableIdentifiedObject(RespondableResource): + """ + An IdentifiedObject to which a Response can be requested. + + :ivar mRID: The global identifier of the object. + :ivar description: The description is a human readable text + describing or naming the object. + :ivar version: Contains the version number of the object. See the + type definition for details. + :ivar subscribable: Indicates whether or not subscriptions are + supported for this resource, and whether or not conditional + (thresholds) are supported. If not specified, is "not + subscribable" (0). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + mRID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + version: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + subscribable: int = field(default=0, metadata={ + "type": "Attribute", + }) + + +@dataclass +class ResponseList(List_type): + """ + A List element to hold Response objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Response: List[Response] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class ResponseListLink(ListLink): + """ + SHALL contain a Link to a List of Response instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ResponseSetListLink(ListLink): + """ + SHALL contain a Link to a List of ResponseSet instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class ServiceSupplier(IdentifiedObject): + """ + Organisation that provides services to Customers. + + :ivar email: E-mail address for this service supplier. + :ivar phone: Human-readable phone number for this service supplier. + :ivar providerID: Contains the IANA PEN for the commodity provider. + :ivar web: Website URI address for this service supplier. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + email: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 32, + }) + phone: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 20, + }) + providerID: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + web: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 42, + }) + + +@dataclass +class SubscribableIdentifiedObject(SubscribableResource): + """ + An IdentifiedObject to which a Subscription can be requested. + + :ivar mRID: The global identifier of the object. + :ivar description: The description is a human readable text + describing or naming the object. + :ivar version: Contains the version number of the object. See the + type definition for details. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + mRID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + version: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class SubscribableList(SubscribableResource): + """ + A List to which a Subscription can be requested. + + :ivar all: The number specifying "all" of the items in the list. + Required on GET, ignored otherwise. + :ivar results: Indicates the number of items in this page of + results. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + all: Optional[int] = field(default=None, metadata={ + "type": "Attribute", + "required": True, + }) + results: Optional[int] = field(default=None, + metadata={ + "type": "Attribute", + "required": True, + }) + + +@dataclass +class Subscription(SubscriptionBase): + """ + Holds the information related to a client subscription to receive updates + to a resource automatically. + + :ivar Condition: + :ivar encoding: 0 - application/sep+xml 1 - application/sep-exi + 2-255 - reserved + :ivar level: Contains the preferred schema and extensibility level + indication such as "+S1" + :ivar limit: This element is used to indicate the maximum number of + list items that should be included in a notification when the + subscribed resource changes. This limit is meant to be + functionally equivalent to the ‘limit’ query string parameter, + but applies to both list resources as well as other resources. + For list resources, if a limit of ‘0’ is specified, then + notifications SHALL contain a list resource with results=’0’ + (equivalent to a simple change notification). For list + resources, if a limit greater than ‘0’ is specified, then + notifications SHALL contain a list resource with results equal + to the limit specified (or less, should the list contain fewer + items than the limit specified or should the server be unable to + provide the requested number of items for any reason) and follow + the same rules for list resources (e.g., ordering). For non- + list resources, if a limit of ‘0’ is specified, then + notifications SHALL NOT contain a resource representation + (equivalent to a simple change notification). For non-list + resources, if a limit greater than ‘0’ is specified, then + notifications SHALL contain the representation of the changed + resource. + :ivar notificationURI: The resource to which to post the + notifications about the requested subscribed resource. Because + this URI will exist on a server other than the one being POSTed + to, this attribute SHALL be a fully-qualified absolute URI, not + a relative reference. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Condition: Optional[Condition] = field(default=None, metadata={ + "type": "Element", + }) + encoding: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + level: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + }) + limit: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + notificationURI: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class SubscriptionListLink(ListLink): + """ + SHALL contain a Link to a List of Subscription instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class SupplyInterruptionOverrideList(List_type): + """ + A List element to hold SupplyInterruptionOverride objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + SupplyInterruptionOverride: List[SupplyInterruptionOverride] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class SupplyInterruptionOverrideListLink(ListLink): + """ + SHALL contain a Link to a List of SupplyInterruptionOverride instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class SupportedLocaleList(List_type): + """ + A List element to hold SupportedLocale objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + SupportedLocale: List[SupportedLocale] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class SupportedLocaleListLink(ListLink): + """ + SHALL contain a Link to a List of SupportedLocale instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TargetReadingListLink(ListLink): + """ + SHALL contain a Link to a List of TargetReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TariffProfileListLink(ListLink): + """ + SHALL contain a Link to a List of TariffProfile instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TextMessageListLink(ListLink): + """ + SHALL contain a Link to a List of TextMessage instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TextResponse(Response): + """ + A response to a text message. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TimeTariffIntervalListLink(ListLink): + """ + SHALL contain a Link to a List of TimeTariffInterval instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class UsagePointBase(IdentifiedObject): + """Logical point on a network at which consumption or production is either + physically measured (e.g. metered) or estimated (e.g. unmetered street + lights). + + A container for associating ReadingType, Readings and ReadingSets. + + :ivar roleFlags: Specifies the roles that apply to the usage point. + :ivar serviceCategoryKind: The kind of service provided by this + usage point. + :ivar status: Specifies the current status of the service at this + usage point. 0 = off 1 = on + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + roleFlags: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 2, + "format": "base16", + }) + serviceCategoryKind: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + status: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class UsagePointListLink(ListLink): + """ + SHALL contain a Link to a List of UsagePoint instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class AbstractDevice(SubscribableResource): + """ + The EndDevice providing the resources available within the + DeviceCapabilities. + + :ivar ConfigurationLink: + :ivar DERListLink: + :ivar deviceCategory: This field is for use in devices that can + adjust energy usage (e.g., demand response, distributed energy + resources). For devices that do not respond to + EndDeviceControls or DERControls (for instance, an ESI), this + field should not have any bits set. + :ivar DeviceInformationLink: + :ivar DeviceStatusLink: + :ivar FileStatusLink: + :ivar IPInterfaceListLink: + :ivar lFDI: Long form of device identifier. See the Security section + for additional details. + :ivar LoadShedAvailabilityListLink: + :ivar LogEventListLink: + :ivar PowerStatusLink: + :ivar sFDI: Short form of device identifier, WITH the checksum + digit. See the Security section for additional details. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ConfigurationLink: Optional[ConfigurationLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERListLink: Optional[DERListLink] = field(default=None, metadata={ + "type": "Element", + }) + deviceCategory: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 4, + "format": "base16", + }) + DeviceInformationLink: Optional[DeviceInformationLink] = field(default=None, + metadata={ + "type": "Element", + }) + DeviceStatusLink: Optional[DeviceStatusLink] = field(default=None, + metadata={ + "type": "Element", + }) + FileStatusLink: Optional[FileStatusLink] = field(default=None, metadata={ + "type": "Element", + }) + IPInterfaceListLink: Optional[IPInterfaceListLink] = field(default=None, + metadata={ + "type": "Element", + }) + lFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 20, + "format": "base16", + }) + LoadShedAvailabilityListLink: Optional[LoadShedAvailabilityListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + LogEventListLink: Optional[LogEventListLink] = field(default=None, + metadata={ + "type": "Element", + }) + PowerStatusLink: Optional[PowerStatusLink] = field(default=None, + metadata={ + "type": "Element", + }) + sFDI: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_inclusive": 281474976710655, + }) + + +@dataclass +class BillingMeterReadingBase(MeterReadingBase): + """ + Contains historical, target, and projection readings of various types, + possibly associated with charges. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + BillingReadingSetListLink: Optional[BillingReadingSetListLink] = field(default=None, + metadata={ + "type": "Element", + }) + ReadingTypeLink: Optional[ReadingTypeLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class BillingPeriodList(SubscribableList): + """ + A List element to hold BillingPeriod objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + BillingPeriod: List[BillingPeriod] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class BillingReadingList(List_type): + """ + A List element to hold BillingReading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + BillingReading: List[BillingReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class BillingReadingSet(ReadingSetBase): + """ + Time sequence of readings of the same reading type. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + BillingReadingListLink: Optional[BillingReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class Configuration(SubscribableResource): + """ + This resource contains various settings to control the operation of the + device. + + :ivar currentLocale: [RFC 4646] identifier of the language-region + currently in use. + :ivar PowerConfiguration: + :ivar PriceResponseCfgListLink: + :ivar TimeConfiguration: + :ivar userDeviceName: User assigned, convenience name used for + network browsing displays, etc. Example "My Thermostat" + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + currentLocale: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 42, + }) + PowerConfiguration: Optional[PowerConfiguration] = field(default=None, + metadata={ + "type": "Element", + }) + PriceResponseCfgListLink: Optional[PriceResponseCfgListLink] = field(default=None, + metadata={ + "type": "Element", + }) + TimeConfiguration: Optional[TimeConfiguration] = field(default=None, + metadata={ + "type": "Element", + }) + userDeviceName: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class CreditRegisterList(List_type): + """ + A List element to hold CreditRegister objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + CreditRegister: List[CreditRegister] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class CustomerAccount(IdentifiedObject): + """Assignment of a group of products and services purchased by the Customer + through a CustomerAgreement, used as a mechanism for customer billing and + payment. + + It contains common information from the various types of + CustomerAgreements to create billings (invoices) for a Customer and + receive payment. + + :ivar currency: The ISO 4217 code indicating the currency applicable + to the bill amounts in the summary. See list at + http://www.unece.org/cefact/recommendations/rec09/rec09_ecetrd203.pdf + :ivar customerAccount: The account number for the customer (if + applicable). + :ivar CustomerAgreementListLink: + :ivar customerName: The name of the customer. + :ivar pricePowerOfTenMultiplier: Indicates the power of ten + multiplier for the prices in this function set. + :ivar ServiceSupplierLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + currency: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + customerAccount: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 42, + }) + CustomerAgreementListLink: Optional[CustomerAgreementListLink] = field(default=None, + metadata={ + "type": "Element", + }) + customerName: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 42, + }) + pricePowerOfTenMultiplier: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + ServiceSupplierLink: Optional[ServiceSupplierLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class CustomerAgreement(IdentifiedObject): + """Agreement between the customer and the service supplier to pay for + service at a specific service location. + + It records certain billing information about the type of service + provided at the service location and is used during charge creation + to determine the type of service. + + :ivar ActiveBillingPeriodListLink: + :ivar ActiveProjectionReadingListLink: + :ivar ActiveTargetReadingListLink: + :ivar BillingPeriodListLink: + :ivar HistoricalReadingListLink: + :ivar PrepaymentLink: + :ivar ProjectionReadingListLink: + :ivar serviceAccount: The account number of the service account (if + applicable). + :ivar serviceLocation: The address or textual description of the + service location. + :ivar TargetReadingListLink: + :ivar TariffProfileLink: + :ivar UsagePointLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ActiveBillingPeriodListLink: Optional[ActiveBillingPeriodListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + ActiveProjectionReadingListLink: Optional[ActiveProjectionReadingListLink] = field( + default=None, metadata={ + "type": "Element", + }) + ActiveTargetReadingListLink: Optional[ActiveTargetReadingListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + BillingPeriodListLink: Optional[BillingPeriodListLink] = field(default=None, + metadata={ + "type": "Element", + }) + HistoricalReadingListLink: Optional[HistoricalReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + PrepaymentLink: Optional[PrepaymentLink] = field(default=None, metadata={ + "type": "Element", + }) + ProjectionReadingListLink: Optional[ProjectionReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + serviceAccount: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 42, + }) + serviceLocation: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 42, + }) + TargetReadingListLink: Optional[TargetReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + TariffProfileLink: Optional[TariffProfileLink] = field(default=None, + metadata={ + "type": "Element", + }) + UsagePointLink: Optional[UsagePointLink] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class DER(SubscribableResource): + """ + Contains links to DER resources. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + AssociatedDERProgramListLink: Optional[AssociatedDERProgramListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + AssociatedUsagePointLink: Optional[AssociatedUsagePointLink] = field(default=None, + metadata={ + "type": "Element", + }) + CurrentDERProgramLink: Optional[CurrentDERProgramLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERAvailabilityLink: Optional[DERAvailabilityLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERCapabilityLink: Optional[DERCapabilityLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERSettingsLink: Optional[DERSettingsLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERStatusLink: Optional[DERStatusLink] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class DERCurveList(List_type): + """ + A List element to hold DERCurve objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DERCurve: List[DERCurve] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class DERProgram(SubscribableIdentifiedObject): + """ + Distributed Energy Resource program. + + :ivar ActiveDERControlListLink: + :ivar DefaultDERControlLink: + :ivar DERControlListLink: + :ivar DERCurveListLink: + :ivar primacy: Indicates the relative primacy of the provider of + this Program. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ActiveDERControlListLink: Optional[ActiveDERControlListLink] = field(default=None, + metadata={ + "type": "Element", + }) + DefaultDERControlLink: Optional[DefaultDERControlLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERControlListLink: Optional[DERControlListLink] = field(default=None, + metadata={ + "type": "Element", + }) + DERCurveListLink: Optional[DERCurveListLink] = field(default=None, + metadata={ + "type": "Element", + }) + primacy: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DefaultDERControl(SubscribableIdentifiedObject): + """ + Contains control mode information to be used if no active DERControl is + found. + + :ivar DERControlBase: + :ivar setESDelay: Enter service delay, in hundredths of a second. + When present, this value SHALL update the value of the + corresponding setting (DERSettings::setESDelay). + :ivar setESHighFreq: Enter service frequency high. Specified in + hundredths of Hz. When present, this value SHALL update the + value of the corresponding setting (DERSettings::setESHighFreq). + :ivar setESHighVolt: Enter service voltage high. Specified as an + effective percent voltage, defined as (100% * (locally measured + voltage - setVRefOfs) / setVRef), in hundredths of a percent. + When present, this value SHALL update the value of the + corresponding setting (DERSettings::setESHighVolt). + :ivar setESLowFreq: Enter service frequency low. Specified in + hundredths of Hz. When present, this value SHALL update the + value of the corresponding setting (DERSettings::setESLowFreq). + :ivar setESLowVolt: Enter service voltage low. Specified as an + effective percent voltage, defined as (100% * (locally measured + voltage - setVRefOfs) / setVRef), in hundredths of a percent. + When present, this value SHALL update the value of the + corresponding setting (DERSettings::setESLowVolt). + :ivar setESRampTms: Enter service ramp time, in hundredths of a + second. When present, this value SHALL update the value of the + corresponding setting (DERSettings::setESRampTms). + :ivar setESRandomDelay: Enter service randomized delay, in + hundredths of a second. When present, this value SHALL update + the value of the corresponding setting + (DERSettings::setESRandomDelay). + :ivar setGradW: Set default rate of change (ramp rate) of active + power output due to command or internal action, defined in + %setWMax / second. Resolution is in hundredths of a + percent/second. A value of 0 means there is no limit. + Interpreted as a percentage change in output capability limit + per second when used as a default ramp rate. When present, this + value SHALL update the value of the corresponding setting + (DERSettings::setGradW). + :ivar setSoftGradW: Set soft-start rate of change (soft-start ramp + rate) of active power output due to command or internal action, + defined in %setWMax / second. Resolution is in hundredths of a + percent/second. A value of 0 means there is no limit. + Interpreted as a percentage change in output capability limit + per second when used as a ramp rate. When present, this value + SHALL update the value of the corresponding setting + (DERSettings::setSoftGradW). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DERControlBase: Optional[DERControlBase] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + setESDelay: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESHighFreq: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESHighVolt: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESLowFreq: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESLowVolt: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESRampTms: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setESRandomDelay: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setGradW: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + setSoftGradW: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class DemandResponseProgram(IdentifiedObject): + """ + Demand response program. + + :ivar ActiveEndDeviceControlListLink: + :ivar availabilityUpdatePercentChangeThreshold: This attribute + allows program providers to specify the requested granularity of + updates to LoadShedAvailability sheddablePercent. If not + present, or set to 0, then updates to LoadShedAvailability SHALL + NOT be provided. If present and greater than zero, then clients + SHALL provide their LoadShedAvailability if it has not + previously been provided, and thereafter if the difference + between the previously provided value and the current value of + LoadShedAvailability sheddablePercent is greater than + availabilityUpdatePercentChangeThreshold. + :ivar availabilityUpdatePowerChangeThreshold: This attribute allows + program providers to specify the requested granularity of + updates to LoadShedAvailability sheddablePower. If not present, + or set to 0, then updates to LoadShedAvailability SHALL NOT be + provided. If present and greater than zero, then clients SHALL + provide their LoadShedAvailability if it has not previously been + provided, and thereafter if the difference between the + previously provided value and the current value of + LoadShedAvailability sheddablePower is greater than + availabilityUpdatePowerChangeThreshold. + :ivar EndDeviceControlListLink: + :ivar primacy: Indicates the relative primacy of the provider of + this program. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ActiveEndDeviceControlListLink: Optional[ActiveEndDeviceControlListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + availabilityUpdatePercentChangeThreshold: Optional[int] = field(default=None, + metadata={ + "type": "Element", + }) + availabilityUpdatePowerChangeThreshold: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + }) + EndDeviceControlListLink: Optional[EndDeviceControlListLink] = field(default=None, + metadata={ + "type": "Element", + }) + primacy: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DeviceInformation(Resource): + """ + Contains identification and other information about the device that changes + very infrequently, typically only when updates are applied, if ever. + + :ivar DRLCCapabilities: + :ivar functionsImplemented: Bitmap indicating the function sets used + by the device as a client. 0 - Device Capability 1 - Self Device + Resource 2 - End Device Resource 3 - Function Set Assignments 4 + - Subscription/Notification Mechanism 5 - Response 6 - Time 7 - + Device Information 8 - Power Status 9 - Network Status 10 - Log + Event 11 - Configuration Resource 12 - Software Download 13 - + DRLC 14 - Metering 15 - Pricing 16 - Messaging 17 - Billing 18 - + Prepayment 19 - Flow Reservation 20 - DER Control + :ivar gpsLocation: GPS location of this device. + :ivar lFDI: Long form device identifier. See the Security section + for full details. + :ivar mfDate: Date/time of manufacture + :ivar mfHwVer: Manufacturer hardware version + :ivar mfID: The manufacturer's IANA Enterprise Number. + :ivar mfInfo: Manufacturer dependent information related to the + manufacture of this device + :ivar mfModel: Manufacturer's model number + :ivar mfSerNum: Manufacturer assigned serial number + :ivar primaryPower: Primary source of power. + :ivar secondaryPower: Secondary source of power + :ivar SupportedLocaleListLink: + :ivar swActTime: Activation date/time of currently running software + :ivar swVer: Currently running software version + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DRLCCapabilities: Optional[DRLCCapabilities] = field(default=None, + metadata={ + "type": "Element", + }) + functionsImplemented: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 8, + "format": "base16", + }) + gpsLocation: Optional[GPSLocationType] = field(default=None, metadata={ + "type": "Element", + }) + lFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 20, + "format": "base16", + }) + mfDate: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + mfHwVer: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + mfID: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + mfInfo: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 32, + }) + mfModel: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + mfSerNum: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + primaryPower: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + secondaryPower: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + SupportedLocaleListLink: Optional[SupportedLocaleListLink] = field(default=None, + metadata={ + "type": "Element", + }) + swActTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + swVer: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 32, + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class Event(RespondableSubscribableIdentifiedObject): + """An Event indicates information that applies to a particular period of + time. + + Events SHALL be executed relative to the time of the server, as + described in the Time function set section 11.1. + + :ivar creationTime: The time at which the Event was created. + :ivar EventStatus: + :ivar interval: The period during which the Event applies. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + creationTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + EventStatus: Optional[EventStatus] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + interval: Optional[DateTimeInterval] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class FlowReservationRequestList(List_type): + """ + A List element to hold FlowReservationRequest objects. + + :ivar FlowReservationRequest: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + FlowReservationRequest: List[FlowReservationRequest] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class FunctionSetAssignmentsBase(Resource): + """ + Defines a collection of function set instances that are to be used by one + or more devices as indicated by the EndDevice object(s) of the server. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + CustomerAccountListLink: Optional[CustomerAccountListLink] = field(default=None, + metadata={ + "type": "Element", + }) + DemandResponseProgramListLink: Optional[DemandResponseProgramListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + DERProgramListLink: Optional[DERProgramListLink] = field(default=None, + metadata={ + "type": "Element", + }) + FileListLink: Optional[FileListLink] = field(default=None, metadata={ + "type": "Element", + }) + MessagingProgramListLink: Optional[MessagingProgramListLink] = field(default=None, + metadata={ + "type": "Element", + }) + PrepaymentListLink: Optional[PrepaymentListLink] = field(default=None, + metadata={ + "type": "Element", + }) + ResponseSetListLink: Optional[ResponseSetListLink] = field(default=None, + metadata={ + "type": "Element", + }) + TariffProfileListLink: Optional[TariffProfileListLink] = field(default=None, + metadata={ + "type": "Element", + }) + TimeLink: Optional[TimeLink] = field(default=None, metadata={ + "type": "Element", + }) + UsagePointListLink: Optional[UsagePointListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class IEEE_802_15_4: + """ + Contains 802.15.4 link layer specific attributes. + + :ivar capabilityInfo: As defined by IEEE 802.15.4 + :ivar NeighborListLink: + :ivar shortAddress: As defined by IEEE 802.15.4 + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + capabilityInfo: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 1, + "format": "base16", + }) + NeighborListLink: Optional[NeighborListLink] = field(default=None, + metadata={ + "type": "Element", + }) + shortAddress: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class IPAddr(Resource): + """ + An Internet Protocol address object. + + :ivar address: An IP address value. + :ivar RPLInstanceListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + address: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + RPLInstanceListLink: Optional[RPLInstanceListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class IPInterface(Resource): + """Specific IPInterface resource. + + This resource may be thought of as network status information for a + specific network (IP) layer interface. + + :ivar ifDescr: Use rules from [RFC 2863]. + :ivar ifHighSpeed: Use rules from [RFC 2863]. + :ivar ifInBroadcastPkts: Use rules from [RFC 2863]. + :ivar ifIndex: Use rules from [RFC 2863]. + :ivar ifInDiscards: Use rules from [RFC 2863]. Can be thought of as + Input Datagrams Discarded. + :ivar ifInErrors: Use rules from [RFC 2863]. + :ivar ifInMulticastPkts: Use rules from [RFC 2863]. Can be thought + of as Multicast Datagrams Received. + :ivar ifInOctets: Use rules from [RFC 2863]. Can be thought of as + Bytes Received. + :ivar ifInUcastPkts: Use rules from [RFC 2863]. Can be thought of as + Datagrams Received. + :ivar ifInUnknownProtos: Use rules from [RFC 2863]. Can be thought + of as Datagrams with Unknown Protocol Received. + :ivar ifMtu: Use rules from [RFC 2863]. + :ivar ifName: Use rules from [RFC 2863]. + :ivar ifOperStatus: Use rules and assignments from [RFC 2863]. + :ivar ifOutBroadcastPkts: Use rules from [RFC 2863]. Can be thought + of as Broadcast Datagrams Sent. + :ivar ifOutDiscards: Use rules from [RFC 2863]. Can be thought of as + Output Datagrams Discarded. + :ivar ifOutErrors: Use rules from [RFC 2863]. + :ivar ifOutMulticastPkts: Use rules from [RFC 2863]. Can be thought + of as Multicast Datagrams Sent. + :ivar ifOutOctets: Use rules from [RFC 2863]. Can be thought of as + Bytes Sent. + :ivar ifOutUcastPkts: Use rules from [RFC 2863]. Can be thought of + as Datagrams Sent. + :ivar ifPromiscuousMode: Use rules from [RFC 2863]. + :ivar ifSpeed: Use rules from [RFC 2863]. + :ivar ifType: Use rules and assignments from [RFC 2863]. + :ivar IPAddrListLink: + :ivar lastResetTime: Similar to ifLastChange in [RFC 2863]. + :ivar lastUpdatedTime: The date/time of the reported status. + :ivar LLInterfaceListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ifDescr: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 192, + }) + ifHighSpeed: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInBroadcastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifIndex: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInDiscards: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInErrors: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInMulticastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInOctets: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInUcastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifInUnknownProtos: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifMtu: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifName: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 16, + }) + ifOperStatus: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutBroadcastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutDiscards: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutErrors: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutMulticastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutOctets: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifOutUcastPkts: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifPromiscuousMode: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + ifSpeed: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + ifType: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + IPAddrListLink: Optional[IPAddrListLink] = field(default=None, metadata={ + "type": "Element", + }) + lastResetTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + lastUpdatedTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLInterfaceListLink: Optional[LLInterfaceListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class LoadShedAvailabilityList(List_type): + """ + A List element to hold LoadShedAvailability objects. + + :ivar LoadShedAvailability: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + LoadShedAvailability: List[LoadShedAvailability] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class LogEventList(SubscribableList): + """ + A List element to hold LogEvent objects. + + :ivar LogEvent: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + LogEvent: List[LogEvent] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class MessagingProgram(SubscribableIdentifiedObject): + """ + Provides a container for collections of text messages. + + :ivar ActiveTextMessageListLink: + :ivar locale: Indicates the language and region of the messages in + this collection. + :ivar primacy: Indicates the relative primacy of the provider of + this program. + :ivar TextMessageListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ActiveTextMessageListLink: Optional[ActiveTextMessageListLink] = field(default=None, + metadata={ + "type": "Element", + }) + locale: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 42, + }) + primacy: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + TextMessageListLink: Optional[TextMessageListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class MeterReading(MeterReadingBase): + """ + Set of values obtained from the meter. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + RateComponentListLink: Optional[RateComponentListLink] = field(default=None, + metadata={ + "type": "Element", + }) + ReadingLink: Optional[ReadingLink] = field(default=None, metadata={ + "type": "Element", + }) + ReadingSetListLink: Optional[ReadingSetListLink] = field(default=None, + metadata={ + "type": "Element", + }) + ReadingTypeLink: Optional[ReadingTypeLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class MirrorReadingSet(ReadingSetBase): + """ + A set of Readings of the ReadingType indicated by the parent MeterReading. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Reading: List[Reading] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class NotificationList(List_type): + """ + A List element to hold Notification objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Notification: List[Notification] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class PriceResponseCfgList(List_type): + """ + A List element to hold PriceResponseCfg objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + PriceResponseCfg: List[PriceResponseCfg] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class RPLInstance(Resource): + """Specific RPLInstance resource. + + This resource may be thought of as network status information for a + specific RPL instance associated with IPInterface. + + :ivar DODAGid: See [RFC 6550]. + :ivar DODAGroot: See [RFC 6550]. + :ivar flags: See [RFC 6550]. + :ivar groundedFlag: See [RFC 6550]. + :ivar MOP: See [RFC 6550]. + :ivar PRF: See [RFC 6550]. + :ivar rank: See [RFC 6550]. + :ivar RPLInstanceID: See [RFC 6550]. + :ivar RPLSourceRoutesListLink: + :ivar versionNumber: See [RFC 6550]. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DODAGid: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + DODAGroot: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + flags: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + groundedFlag: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + MOP: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + PRF: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + rank: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + RPLInstanceID: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + RPLSourceRoutesListLink: Optional[RPLSourceRoutesListLink] = field(default=None, + metadata={ + "type": "Element", + }) + versionNumber: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class RateComponent(IdentifiedObject): + """ + Specifies the applicable charges for a single component of the rate, which + could be generation price or consumption price, for example. + + :ivar ActiveTimeTariffIntervalListLink: + :ivar flowRateEndLimit: Specifies the maximum flow rate (e.g. kW for + electricity) for which this RateComponent applies, for the usage + point and given rate / tariff. In combination with + flowRateStartLimit, allows a service provider to define the + demand or output characteristics for the particular tariff + design. If a server includes the flowRateEndLimit attribute, + then it SHALL also include flowRateStartLimit attribute. For + example, a service provider’s tariff limits customers to 20 kWs + of demand for the given rate structure. Above this threshold + (from 20-50 kWs), there are different demand charges per unit of + consumption. The service provider can use flowRateStartLimit + and flowRateEndLimit to describe the demand characteristics of + the different rates. Similarly, these attributes can be used to + describe limits on premises DERs that might be producing a + commodity and sending it back into the distribution network. + Note: At the time of writing, service provider tariffs with + demand-based components were not originally identified as being + in scope, and service provider tariffs vary widely in their use + of demand components and the method for computing charges. It + is expected that industry groups (e.g., OpenSG) will document + requirements in the future that the IEEE 2030.5 community can + then use as source material for the next version of IEEE 2030.5. + :ivar flowRateStartLimit: Specifies the minimum flow rate (e.g., kW + for electricity) for which this RateComponent applies, for the + usage point and given rate / tariff. In combination with + flowRateEndLimit, allows a service provider to define the demand + or output characteristics for the particular tariff design. If + a server includes the flowRateStartLimit attribute, then it + SHALL also include flowRateEndLimit attribute. + :ivar ReadingTypeLink: Provides indication of the ReadingType with + which this price is associated. + :ivar roleFlags: Specifies the roles that this usage point has been + assigned. + :ivar TimeTariffIntervalListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ActiveTimeTariffIntervalListLink: Optional[ActiveTimeTariffIntervalListLink] = field( + default=None, metadata={ + "type": "Element", + }) + flowRateEndLimit: Optional[UnitValueType] = field(default=None, metadata={ + "type": "Element", + }) + flowRateStartLimit: Optional[UnitValueType] = field(default=None, + metadata={ + "type": "Element", + }) + ReadingTypeLink: Optional[ReadingTypeLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + roleFlags: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 2, + "format": "base16", + }) + TimeTariffIntervalListLink: Optional[TimeTariffIntervalListLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class ReadingList(SubscribableList): + """ + A List element to hold Reading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Reading: List[Reading] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class ReadingSet(ReadingSetBase): + """ + A set of Readings of the ReadingType indicated by the parent MeterReading. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ReadingListLink: Optional[ReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class ResponseSet(IdentifiedObject): + """ + A container for a ResponseList. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ResponseListLink: Optional[ResponseListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class ServiceSupplierList(List_type): + """ + A List element to hold ServiceSupplier objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ServiceSupplier: List[ServiceSupplier] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class SubscriptionList(List_type): + """ + A List element to hold Subscription objects. + + :ivar Subscription: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Subscription: List[Subscription] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class TariffProfile(IdentifiedObject): + """ + A schedule of charges; structure that allows the definition of tariff + structures such as step (block) and time of use (tier) when used in + conjunction with TimeTariffInterval and ConsumptionTariffInterval. + + :ivar currency: The currency code indicating the currency for this + TariffProfile. + :ivar pricePowerOfTenMultiplier: Indicates the power of ten + multiplier for the price attribute. + :ivar primacy: Indicates the relative primacy of the provider of + this program. + :ivar rateCode: The rate code for this tariff profile. Provided by + the Pricing service provider per its internal business needs and + practices and provides a method to identify the specific rate + code for the TariffProfile instance. This would typically not + be communicated to the user except to facilitate troubleshooting + due to its service provider-specific technical nature. + :ivar RateComponentListLink: + :ivar serviceCategoryKind: The kind of service provided by this + usage point. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + currency: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + pricePowerOfTenMultiplier: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + primacy: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + rateCode: Optional[str] = field(default=None, metadata={ + "type": "Element", + "max_length": 20, + }) + RateComponentListLink: Optional[RateComponentListLink] = field(default=None, + metadata={ + "type": "Element", + }) + serviceCategoryKind: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class UsagePoint(UsagePointBase): + """ + Logical point on a network at which consumption or production is either + physically measured (e.g. metered) or estimated (e.g. unmetered street + lights). + + :ivar deviceLFDI: The LFDI of the source device. This attribute + SHALL be present when mirroring. + :ivar MeterReadingListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + deviceLFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 20, + "format": "base16", + }) + MeterReadingListLink: Optional[MeterReadingListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class BillingReadingSetList(SubscribableList): + """ + A List element to hold BillingReadingSet objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + BillingReadingSet: List[BillingReadingSet] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class CustomerAccountList(SubscribableList): + """ + A List element to hold CustomerAccount objects. + + :ivar CustomerAccount: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + CustomerAccount: List[CustomerAccount] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class CustomerAgreementList(SubscribableList): + """ + A List element to hold CustomerAgreement objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + CustomerAgreement: List[CustomerAgreement] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class DERList(List_type): + """ + A List element to hold DER objects. + + :ivar DER: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DER: List[DER] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class DERProgramList(SubscribableList): + """ + A List element to hold DERProgram objects. + + :ivar DERProgram: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DERProgram: List[DERProgram] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class DemandResponseProgramList(SubscribableList): + """ + A List element to hold DemandResponseProgram objects. + + :ivar DemandResponseProgram: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DemandResponseProgram: List[DemandResponseProgram] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class DeviceCapability(FunctionSetAssignmentsBase): + """ + Returned by the URI provided by DNS-SD, to allow clients to find the URIs + to the resources in which they are interested. + + :ivar EndDeviceListLink: + :ivar MirrorUsagePointListLink: + :ivar SelfDeviceLink: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + EndDeviceListLink: Optional[EndDeviceListLink] = field(default=None, + metadata={ + "type": "Element", + }) + MirrorUsagePointListLink: Optional[MirrorUsagePointListLink] = field(default=None, + metadata={ + "type": "Element", + }) + SelfDeviceLink: Optional[SelfDeviceLink] = field(default=None, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class EndDevice(AbstractDevice): + """Asset container that performs one or more end device functions. + + Contains information about individual devices in the network. + + :ivar changedTime: The time at which this resource was last modified + or created. + :ivar enabled: This attribute indicates whether or not an EndDevice + is enabled, or registered, on the server. If a server sets this + attribute to false, the device is no longer registered. It + should be noted that servers can delete EndDevice instances, but + using this attribute for some time is more convenient for + clients. + :ivar FlowReservationRequestListLink: + :ivar FlowReservationResponseListLink: + :ivar FunctionSetAssignmentsListLink: + :ivar postRate: POST rate, or how often EndDevice and subordinate + resources should be POSTed, in seconds. A client MAY indicate a + preferred postRate when POSTing EndDevice. A server MAY add or + modify postRate to indicate its preferred posting rate. + :ivar RegistrationLink: + :ivar SubscriptionListLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + changedTime: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + enabled: Optional[bool] = field(default=None, metadata={ + "type": "Element", + }) + FlowReservationRequestListLink: Optional[FlowReservationRequestListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + FlowReservationResponseListLink: Optional[FlowReservationResponseListLink] = field( + default=None, metadata={ + "type": "Element", + }) + FunctionSetAssignmentsListLink: Optional[FunctionSetAssignmentsListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + postRate: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + RegistrationLink: Optional[RegistrationLink] = field(default=None, + metadata={ + "type": "Element", + }) + SubscriptionListLink: Optional[SubscriptionListLink] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class FlowReservationResponse(Event): + """ + The server may modify the charging or discharging parameters and interval + to provide a lower aggregated demand at the premises, or within a larger + part of the distribution system. + + :ivar energyAvailable: Indicates the amount of energy available. + :ivar powerAvailable: Indicates the amount of power available. + :ivar subject: The subject field provides a method to match the + response with the originating event. It is populated with the + mRID of the corresponding FlowReservationRequest object. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + energyAvailable: Optional[SignedRealEnergy] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + powerAvailable: Optional[ActivePower] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + subject: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + + +@dataclass +class FunctionSetAssignments(FunctionSetAssignmentsBase): + """ + Provides an identifiable, subscribable collection of resources for a + particular device to consume. + + :ivar mRID: The global identifier of the object. + :ivar description: The description is a human readable text + describing or naming the object. + :ivar version: Contains the version number of the object. See the + type definition for details. + :ivar subscribable: Indicates whether or not subscriptions are + supported for this resource, and whether or not conditional + (thresholds) are supported. If not specified, is "not + subscribable" (0). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + mRID: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 16, + "format": "base16", + }) + description: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 32, + }) + version: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + subscribable: int = field(default=0, metadata={ + "type": "Attribute", + }) + + +@dataclass +class HistoricalReading(BillingMeterReadingBase): + """To be used to present readings that have been processed and possibly + corrected (as allowed, due to missing or incorrect data) by backend + systems. + + This includes quality codes valid, verified, estimated, and derived + / corrected. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class IPAddrList(List_type): + """ + List of IPAddr instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + IPAddr: List[IPAddr] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class IPInterfaceList(List_type): + """ + List of IPInterface instances. + + :ivar IPInterface: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + IPInterface: List[IPInterface] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class LLInterface(Resource): + """ + A link-layer interface object. + + :ivar CRCerrors: Contains the number of CRC errors since reset. + :ivar EUI64: Contains the EUI-64 of the link layer interface. 48 bit + MAC addresses SHALL be changed into an EUI-64 using the method + defined in [RFC 4291], Appendix A. (The method is to insert + "0xFFFE" as described in the reference.) + :ivar IEEE_802_15_4: + :ivar linkLayerType: Specifies the type of link layer interface + associated with the IPInterface. Values are below. 0 = + Unspecified 1 = IEEE 802.3 (Ethernet) 2 = IEEE 802.11 (WLAN) 3 = + IEEE 802.15 (PAN) 4 = IEEE 1901 (PLC) All other values reserved. + :ivar LLAckNotRx: Number of times an ACK was not received for a + frame transmitted (when ACK was requested). + :ivar LLCSMAFail: Number of times CSMA failed. + :ivar LLFramesDropRx: Number of dropped receive frames. + :ivar LLFramesDropTx: Number of dropped transmit frames. + :ivar LLFramesRx: Number of link layer frames received. + :ivar LLFramesTx: Number of link layer frames transmitted. + :ivar LLMediaAccessFail: Number of times access to media failed. + :ivar LLOctetsRx: Number of Bytes received. + :ivar LLOctetsTx: Number of Bytes transmitted. + :ivar LLRetryCount: Number of MAC transmit retries. + :ivar LLSecurityErrorRx: Number of receive security errors. + :ivar loWPAN: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + CRCerrors: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + EUI64: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 8, + "format": "base16", + }) + IEEE_802_15_4: Optional[IEEE_802_15_4] = field(default=None, metadata={ + "type": "Element", + }) + linkLayerType: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + LLAckNotRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLCSMAFail: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLFramesDropRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLFramesDropTx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLFramesRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLFramesTx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLMediaAccessFail: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLOctetsRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLOctetsTx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLRetryCount: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + LLSecurityErrorRx: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + loWPAN: Optional[loWPAN] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class MessagingProgramList(SubscribableList): + """ + A List element to hold MessagingProgram objects. + + :ivar MessagingProgram: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + MessagingProgram: List[MessagingProgram] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class MeterReadingList(SubscribableList): + """ + A List element to hold MeterReading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + MeterReading: List[MeterReading] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class MirrorMeterReading(MeterReadingBase): + """ + Mimic of MeterReading used for managing mirrors. + + :ivar lastUpdateTime: The date and time of the last update. + :ivar MirrorReadingSet: + :ivar nextUpdateTime: The date and time of the next planned update. + :ivar Reading: + :ivar ReadingType: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + lastUpdateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + MirrorReadingSet: List[MirrorReadingSet] = field(default_factory=list, + metadata={ + "type": "Element", + }) + nextUpdateTime: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + Reading: Optional[Reading] = field(default=None, metadata={ + "type": "Element", + }) + ReadingType: Optional[ReadingType] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class Prepayment(IdentifiedObject): + """ + Prepayment (inherited from CIM SDPAccountingFunction) + + :ivar AccountBalanceLink: + :ivar ActiveCreditRegisterListLink: + :ivar ActiveSupplyInterruptionOverrideListLink: + :ivar creditExpiryLevel: CreditExpiryLevel is the set point for + availableCredit at which the service level may be changed. The + typical value for this attribute is 0, regardless of whether the + account balance is measured in a monetary or commodity basis. + The units for this attribute SHALL match the units used for + availableCredit. + :ivar CreditRegisterListLink: + :ivar lowCreditWarningLevel: LowCreditWarningLevel is the set point + for availableCredit at which the creditStatus attribute in the + AccountBalance resource SHALL indicate that available credit is + low. The units for this attribute SHALL match the units used for + availableCredit. Typically, this value is set by the service + provider. + :ivar lowEmergencyCreditWarningLevel: LowEmergencyCreditWarningLevel + is the set point for emergencyCredit at which the creditStatus + attribute in the AccountBalance resource SHALL indicate that + emergencycredit is low. The units for this attribute SHALL match + the units used for availableCredit. Typically, this value is set + by the service provider. + :ivar prepayMode: PrepayMode specifies whether the given Prepayment + instance is operating in Credit, Central Wallet, ESI, or Local + prepayment mode. The Credit mode indicates that prepayment is + not presently in effect. The other modes are described in the + Overview Section above. + :ivar PrepayOperationStatusLink: + :ivar SupplyInterruptionOverrideListLink: + :ivar UsagePoint: + :ivar UsagePointLink: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + AccountBalanceLink: Optional[AccountBalanceLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + ActiveCreditRegisterListLink: Optional[ActiveCreditRegisterListLink] = field(default=None, + metadata={ + "type": + "Element", + }) + ActiveSupplyInterruptionOverrideListLink: Optional[ + ActiveSupplyInterruptionOverrideListLink] = field(default=None, + metadata={ + "type": "Element", + }) + creditExpiryLevel: Optional[AccountingUnit] = field(default=None, + metadata={ + "type": "Element", + }) + CreditRegisterListLink: Optional[CreditRegisterListLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + lowCreditWarningLevel: Optional[AccountingUnit] = field(default=None, + metadata={ + "type": "Element", + }) + lowEmergencyCreditWarningLevel: Optional[AccountingUnit] = field(default=None, + metadata={ + "type": "Element", + }) + prepayMode: Optional[int] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + PrepayOperationStatusLink: Optional[PrepayOperationStatusLink] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + SupplyInterruptionOverrideListLink: Optional[SupplyInterruptionOverrideListLink] = field( + default=None, metadata={ + "type": "Element", + "required": True, + }) + UsagePoint: List[UsagePoint] = field(default_factory=list, metadata={ + "type": "Element", + }) + UsagePointLink: Optional[UsagePointLink] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class ProjectionReading(BillingMeterReadingBase): + """ + Contains values that forecast a future reading for the time or interval + specified. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class RPLInstanceList(List_type): + """ + List of RPLInstances associated with the IPinterface. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + RPLInstance: List[RPLInstance] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class RandomizableEvent(Event): + """ + An Event that can indicate time ranges over which the start time and + duration SHALL be randomized. + + :ivar randomizeDuration: Number of seconds boundary inside which a + random value must be selected to be applied to the associated + interval duration, to avoid sudden synchronized demand changes. + If related to price level changes, sign may be ignored. Valid + range is -3600 to 3600. If not specified, 0 is the default. + :ivar randomizeStart: Number of seconds boundary inside which a + random value must be selected to be applied to the associated + interval start time, to avoid sudden synchronized demand + changes. If related to price level changes, sign may be ignored. + Valid range is -3600 to 3600. If not specified, 0 is the + default. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + randomizeDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + randomizeStart: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class RateComponentList(List_type): + """ + A List element to hold RateComponent objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + RateComponent: List[RateComponent] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class ReadingSetList(SubscribableList): + """ + A List element to hold ReadingSet objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ReadingSet: List[ReadingSet] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class ResponseSetList(List_type): + """ + A List element to hold ResponseSet objects. + + :ivar ResponseSet: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ResponseSet: List[ResponseSet] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class SelfDevice(AbstractDevice): + """ + The EndDevice providing the resources available within the + DeviceCapabilities. + + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class TargetReading(BillingMeterReadingBase): + """ + Contains readings that specify a target or goal, such as a consumption + target, to which billing incentives or other contractual ramifications may + be associated. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + +@dataclass +class TariffProfileList(SubscribableList): + """ + A List element to hold TariffProfile objects. + + :ivar TariffProfile: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + TariffProfile: List[TariffProfile] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class TextMessage(Event): + """ + Text message such as a notification. + + :ivar originator: Indicates the human-readable name of the publisher + of the message + :ivar priority: The priority is used to inform the client of the + priority of the particular message. Devices with constrained or + limited resources for displaying Messages should use this + attribute to determine how to handle displaying currently active + Messages (e.g. if a device uses a scrolling method with a single + Message viewable at a time it MAY want to push a low priority + Message to the background and bring a newly received higher + priority Message to the foreground). + :ivar textMessage: The textMessage attribute contains the actual + UTF-8 encoded text to be displayed in conjunction with the + messageLength attribute which contains the overall length of the + textMessage attribute. Clients and servers SHALL support a + reception of a Message of 100 bytes in length. Messages that + exceed the clients display size will be left to the client to + choose what method to handle the message (truncation, scrolling, + etc.). + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + originator: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "max_length": 20, + }) + priority: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + textMessage: Optional[str] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class UsagePointList(SubscribableList): + """ + A List element to hold UsagePoint objects. + + :ivar UsagePoint: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + UsagePoint: List[UsagePoint] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class DERControl(RandomizableEvent): + """ + Distributed Energy Resource (DER) time/event-based control. + + :ivar DERControlBase: + :ivar deviceCategory: Specifies the bitmap indicating the + categories of devices that SHOULD respond. Devices SHOULD ignore + events that do not indicate their device category. If not + present, all devices SHOULD respond. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DERControlBase: Optional[DERControlBase] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + deviceCategory: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "max_length": 4, + "format": "base16", + }) + + +@dataclass +class EndDeviceControl(RandomizableEvent): + """ + Instructs an EndDevice to perform a specified action. + + :ivar ApplianceLoadReduction: + :ivar deviceCategory: Specifies the bitmap indicating the + categories of devices that SHOULD respond. Devices SHOULD ignore + events that do not indicate their device category. + :ivar drProgramMandatory: A flag to indicate if the EndDeviceControl + is considered a mandatory event as defined by the service + provider issuing the EndDeviceControl. The drProgramMandatory + flag alerts the client/user that they will be subject to penalty + or ineligibility based on the service provider’s program rules + for that deviceCategory. + :ivar DutyCycle: + :ivar loadShiftForward: Indicates that the event intends to increase + consumption. A value of true indicates the intention to increase + usage value, and a value of false indicates the intention to + decrease usage. + :ivar Offset: + :ivar overrideDuration: The overrideDuration attribute provides a + duration, in seconds, for which a client device is allowed to + override this EndDeviceControl and still meet the contractual + agreement with a service provider without opting out. If + overrideDuration is not specified, then it SHALL default to 0. + :ivar SetPoint: + :ivar TargetReduction: + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ApplianceLoadReduction: Optional[ApplianceLoadReduction] = field(default=None, + metadata={ + "type": "Element", + }) + deviceCategory: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 4, + "format": "base16", + }) + drProgramMandatory: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + DutyCycle: Optional[DutyCycle] = field(default=None, metadata={ + "type": "Element", + }) + loadShiftForward: Optional[bool] = field(default=None, + metadata={ + "type": "Element", + "required": True, + }) + Offset: Optional[Offset] = field(default=None, metadata={ + "type": "Element", + }) + overrideDuration: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + SetPoint: Optional[SetPoint] = field(default=None, metadata={ + "type": "Element", + }) + TargetReduction: Optional[TargetReduction] = field(default=None, + metadata={ + "type": "Element", + }) + + +@dataclass +class EndDeviceList(SubscribableList): + """ + A List element to hold EndDevice objects. + + :ivar EndDevice: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + EndDevice: List[EndDevice] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class FlowReservationResponseList(SubscribableList): + """ + A List element to hold FlowReservationResponse objects. + + :ivar FlowReservationResponse: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + FlowReservationResponse: List[FlowReservationResponse] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class FunctionSetAssignmentsList(SubscribableList): + """ + A List element to hold FunctionSetAssignments objects. + + :ivar FunctionSetAssignments: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + FunctionSetAssignments: List[FunctionSetAssignments] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class HistoricalReadingList(List_type): + """ + A List element to hold HistoricalReading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + HistoricalReading: List[HistoricalReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class LLInterfaceList(List_type): + """ + List of LLInterface instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + LLInterface: List[LLInterface] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class MirrorMeterReadingList(List_type): + """ + A List of MirrorMeterReading instances. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + MirrorMeterReading: List[MirrorMeterReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class MirrorUsagePoint(UsagePointBase): + """ + A parallel to UsagePoint to support mirroring. + + :ivar deviceLFDI: The LFDI of the device being mirrored. + :ivar MirrorMeterReading: + :ivar postRate: POST rate, or how often mirrored data should be + POSTed, in seconds. A client MAY indicate a preferred postRate + when POSTing MirrorUsagePoint. A server MAY add or modify + postRate to indicate its preferred posting rate. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + deviceLFDI: Optional[bytes] = field(default=None, + metadata={ + "type": "Element", + "required": True, + "max_length": 20, + "format": "base16", + }) + MirrorMeterReading: List[MirrorMeterReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + postRate: Optional[int] = field(default=None, metadata={ + "type": "Element", + }) + + +@dataclass +class PrepaymentList(SubscribableList): + """ + A List element to hold Prepayment objects. + + :ivar Prepayment: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + Prepayment: List[Prepayment] = field(default_factory=list, metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class ProjectionReadingList(List_type): + """ + A List element to hold ProjectionReading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ProjectionReading: List[ProjectionReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class TargetReadingList(List_type): + """ + A List element to hold TargetReading objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + TargetReading: List[TargetReading] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class TextMessageList(SubscribableList): + """ + A List element to hold TextMessage objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + TextMessage: List[TextMessage] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class TimeTariffInterval(RandomizableEvent): + """ + Describes the time-differentiated portion of the RateComponent, if + applicable, and provides the ability to specify multiple time intervals, + each with its own consumption-based components and other attributes. + + :ivar ConsumptionTariffIntervalListLink: + :ivar touTier: Indicates the time of use tier related to the + reading. If not specified, is assumed to be "0 - N/A". + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + ConsumptionTariffIntervalListLink: Optional[ConsumptionTariffIntervalListLink] = field( + default=None, metadata={ + "type": "Element", + }) + touTier: Optional[int] = field(default=None, metadata={ + "type": "Element", + "required": True, + }) + + +@dataclass +class DERControlList(SubscribableList): + """ + A List element to hold DERControl objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + DERControl: List[DERControl] = field(default_factory=list, metadata={ + "type": "Element", + }) + + +@dataclass +class EndDeviceControlList(SubscribableList): + """ + A List element to hold EndDeviceControl objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + EndDeviceControl: List[EndDeviceControl] = field(default_factory=list, + metadata={ + "type": "Element", + }) + + +@dataclass +class MirrorUsagePointList(List_type): + """ + A List of MirrorUsagePoint instances. + + :ivar MirrorUsagePoint: + :ivar pollRate: The default polling rate for this function set (this + resource and all resources below), in seconds. If not specified, + a default of 900 seconds (15 minutes) is used. It is RECOMMENDED + a client poll the resources of this function set every pollRate + seconds. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + MirrorUsagePoint: List[MirrorUsagePoint] = field(default_factory=list, + metadata={ + "type": "Element", + }) + pollRate: int = field(default=900, metadata={ + "type": "Attribute", + }) + + +@dataclass +class TimeTariffIntervalList(SubscribableList): + """ + A List element to hold TimeTariffInterval objects. + """ + + class Meta: + namespace = "urn:ieee:std:2030.5:ns" + + TimeTariffInterval: List[TimeTariffInterval] = field(default_factory=list, + metadata={ + "type": "Element", + }) diff --git a/src/python/otsim/ieee_2030_5/client_helper/models/tree.py b/src/python/otsim/ieee_2030_5/client_helper/models/tree.py new file mode 100644 index 0000000..fb48405 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/models/tree.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass + +from dataclasses_json import dataclass_json + + +@dataclass_json +@dataclass +class DataTree: + index: int + dc: dataclass + href: str diff --git a/src/python/otsim/ieee_2030_5/client_helper/protocol_models.py b/src/python/otsim/ieee_2030_5/client_helper/protocol_models.py new file mode 100644 index 0000000..fe8e9db --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/protocol_models.py @@ -0,0 +1,213 @@ +"""Minimal IEEE 2030.5 protocol model profile used by this repository. + +This module intentionally models a focused subset of IEEE 2030.5 resources +that are required by the current standalone scheduler + device flow. +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, List, Optional + + +def _pick(payload: Dict[str, Any], *keys: str, default: Any = None) -> Any: + for key in keys: + if key in payload: + return payload[key] + return default + + +@dataclass +class DeviceIdentity: + sfdi: str + lfdi: str + pin_code: str + categories: List[str] = field(default_factory=list) + enabled: bool = True + + +@dataclass +class DerCapability: + rtg_w: float + rtg_va: float + max_charge_rate_w: Optional[float] = None + max_discharge_rate_w: Optional[float] = None + supported_modes: List[str] = field(default_factory=list) + + +@dataclass +class DerControlBase: + op_mod_connect: Optional[bool] = None + op_mod_energize: Optional[bool] = None + op_mod_fixed_w: Optional[float] = None + op_mod_fixed_pf_absorb_w: Optional[float] = None + op_mod_fixed_pf_inject_w: Optional[float] = None + set_grad_w: Optional[float] = None + set_es_delay: Optional[int] = None + + @staticmethod + def from_api_payload(payload: Dict[str, Any]) -> "DerControlBase": + return DerControlBase( + op_mod_connect=_pick(payload, "opModConnect", "op_mod_connect"), + op_mod_energize=_pick(payload, "opModEnergize", "op_mod_energize"), + op_mod_fixed_w=_pick(payload, "opModFixedW", "op_mod_fixed_w"), + op_mod_fixed_pf_absorb_w=_pick(payload, "opModFixedPFAbsorbW", + "op_mod_fixed_pf_absorb_w"), + op_mod_fixed_pf_inject_w=_pick(payload, "opModFixedPFInjectW", + "op_mod_fixed_pf_inject_w"), + set_grad_w=_pick(payload, "setGradW", "set_grad_w"), + set_es_delay=_pick(payload, "setESDelay", "set_es_delay"), + ) + + def to_api_payload(self) -> Dict[str, Any]: + payload = { + "opModConnect": self.op_mod_connect, + "opModEnergize": self.op_mod_energize, + "opModFixedW": self.op_mod_fixed_w, + "opModFixedPFAbsorbW": self.op_mod_fixed_pf_absorb_w, + "opModFixedPFInjectW": self.op_mod_fixed_pf_inject_w, + "setGradW": self.set_grad_w, + "setESDelay": self.set_es_delay, + } + return {k: v for k, v in payload.items() if v is not None} + + +@dataclass +class TimeWindow: + start_time_utc: str + duration_sec: int + + @staticmethod + def from_api_payload(payload: Dict[str, Any]) -> "TimeWindow": + return TimeWindow(start_time_utc=_pick(payload, "start", "start_time_utc"), + duration_sec=int(_pick(payload, "durationSec", "duration_sec"))) + + def to_api_payload(self) -> Dict[str, Any]: + return {"start": self.start_time_utc, "durationSec": self.duration_sec} + + +@dataclass +class DerControlEvent: + event_id: str + interval: TimeWindow + control_base: DerControlBase + + @staticmethod + def from_api_payload(payload: Dict[str, Any]) -> "DerControlEvent": + interval = TimeWindow.from_api_payload(payload["interval"]) + control = DerControlBase.from_api_payload(_pick(payload, "DERControlBase", "control_base", + default={})) + return DerControlEvent(event_id=_pick(payload, "eventId", "event_id"), + interval=interval, + control_base=control) + + def to_api_payload(self) -> Dict[str, Any]: + return { + "eventId": self.event_id, + "interval": self.interval.to_api_payload(), + "DERControlBase": self.control_base.to_api_payload(), + } + + +@dataclass +class DerProgramModel: + program_id: str + default_control: DerControlBase + events: List[DerControlEvent] = field(default_factory=list) + + @staticmethod + def from_api_payload(payload: Dict[str, Any]) -> "DerProgramModel": + container = payload.get("program", payload) + raw_default = _pick(container, "defaultControl", "default_control", default={}) + raw_events = _pick(container, "events", default=[]) + return DerProgramModel( + program_id=_pick(container, "programId", "program_id"), + default_control=DerControlBase.from_api_payload(raw_default), + events=[DerControlEvent.from_api_payload(x) for x in raw_events], + ) + + def to_api_derp_response(self) -> Dict[str, Any]: + return { + "program": { + "programId": self.program_id, + "defaultControl": self.default_control.to_api_payload(), + } + } + + def to_api_derc_response(self) -> Dict[str, Any]: + return {"controls": [event.to_api_payload() for event in self.events]} + + +@dataclass +class MirrorUsageSample: + sfdi: str + ts_utc: str + soc_pct: Optional[float] = None + p_w: Optional[float] = None + q_var: Optional[float] = None + status: Optional[str] = None + + @staticmethod + def from_api_payload(payload: Dict[str, Any]) -> "MirrorUsageSample": + return MirrorUsageSample( + sfdi=payload["sfdi"], + ts_utc=_pick(payload, "ts", "ts_utc"), + soc_pct=_pick(payload, "socPct", "soc_pct"), + p_w=_pick(payload, "pW", "p_w"), + q_var=_pick(payload, "qVar", "q_var"), + status=payload.get("status"), + ) + + def to_api_payload(self) -> Dict[str, Any]: + payload = { + "sfdi": self.sfdi, + "ts": self.ts_utc, + "socPct": self.soc_pct, + "pW": self.p_w, + "qVar": self.q_var, + "status": self.status, + } + return {k: v for k, v in payload.items() if v is not None} + + +def model_to_dict(model: Any) -> Dict[str, Any]: + return asdict(model) + + +def create_peak_hours_sample_program() -> DerProgramModel: + default = DerControlBase(op_mod_connect=True, + op_mod_energize=True, + op_mod_fixed_pf_inject_w=0.98, + op_mod_fixed_pf_absorb_w=0.98, + set_grad_w=1500, + set_es_delay=0) + events = [ + DerControlEvent( + event_id="pre-peak-ramp-up", + interval=TimeWindow(start_time_utc="2026-04-24T16:30:00Z", duration_sec=1800), + control_base=DerControlBase(op_mod_connect=True, + op_mod_energize=True, + op_mod_fixed_w=25000, + set_grad_w=1200), + ), + DerControlEvent( + event_id="peak-hold", + interval=TimeWindow(start_time_utc="2026-04-24T17:00:00Z", duration_sec=7200), + control_base=DerControlBase(op_mod_fixed_w=40000, set_grad_w=0), + ), + DerControlEvent( + event_id="post-peak-ramp-down", + interval=TimeWindow(start_time_utc="2026-04-24T19:00:00Z", duration_sec=1800), + control_base=DerControlBase(op_mod_fixed_w=5000, set_grad_w=1200), + ), + ] + return DerProgramModel(program_id="peak-hours-v1", default_control=default, events=events) + + +def create_sample_mup() -> MirrorUsageSample: + return MirrorUsageSample(sfdi="111222333444", + ts_utc="2026-04-24T17:15:00Z", + soc_pct=62.5, + p_w=18750, + q_var=1500, + status="ON_GRID") diff --git a/src/python/otsim/ieee_2030_5/client_helper/types_/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/types_/__init__.py new file mode 100644 index 0000000..28718f0 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/types_/__init__.py @@ -0,0 +1,81 @@ +import calendar +import time +from dataclasses import dataclass +from datetime import datetime, timedelta +from enum import IntEnum +from pathlib import Path +from typing import Union, Any, List, Dict + +PathStr = Union[Path, str] +StrPath = PathStr +TimeType = int +TimeOffsetType = int +Lfdi = str + +SEP_XML = "application/sep+xml" + + +def format_time(dt_obj: datetime, is_local: bool = False) -> TimeType: + """ Return a proper IEEE2030_5 TimeType object for the dt_obj passed in. + From IEEE 2030.5 spec: + TimeType Object (Int64) + Time is a signed 64 bit value representing the number of seconds + since 0 hours, 0 minutes, 0 seconds, on the 1st of January, 1970, + in UTC, not counting leap seconds. + :param dt_obj: Datetime object to convert to IEEE2030_5 TimeType object. + :param is_local: dt_obj is in UTC or Local time. Default to UTC time. + :return: Time XSD object + :raises: If utc_dt_obj is not UTC + """ + + if dt_obj.tzinfo is None: + raise Exception("IEEE 2030.5 times should be timezone aware UTC or local") + + if dt_obj.utcoffset() != timedelta(0) and not is_local: + raise Exception("IEEE 2030.5 TimeType should be based on UTC") + + if is_local: + return TimeType(int(time.mktime(dt_obj.timetuple()))) + else: + return TimeType(int(calendar.timegm(dt_obj.timetuple()))) + + +class DERControlType(IntEnum): + # Control modes supported by the DER. Bit + # positions SHALL be defined as follows: + # 0 - Charge mode + chargeMode = 0 + # 1 - Discharge mode + dischargeMode = 1 + # 2 - opModConnect (Connect / Disconnect - + # implies galvanic isolation) + opModConnect = 2 + # 3 - opModEnergize (Energize / De-Energize) + opModEnergize = 3 + # 4 - opModFixedPFAbsorbW (Fixed Power + opModFixedPFAbsorb = 4 + # Factor Setpoint when absorbing active # power) + # 5 - opModFixedPFInjectW (Fixed Power + opModFixedPFInject = 5 + # Factor Setpoint when injecting active power) + # 6 - opModFixedVar (Reactive Power Setpoint) + opModFixedVar = 6 + # 7 - opModFixedW (Charge / Discharge Setpoint) + opModFixedW = 7 + # 8 - opModFreqDroop (Frequency-Watt Parameterized Mode) + opModFreqDroop = 8 + # 9 - opModFreqWatt (Frequency-Watt Curve Mode) + opModFreqWatt = 9 + # 10 - opModHFRTMayTrip (High Frequency Ride Through, May Trip Mode) + opModHFRTMayTrip = 10 + # 11 - opModHFRTMustTrip (High Frequency Ride Through, Must Trip Mode) + opModHFRTMustTrip = 11 + # 12 - opModHVRTMayTrip (High Voltage Ride Through, May Trip Mode) + opModHVRTMayTrip = 12 + # 13 - opModHVRTMomentaryCessation (High Voltage Ride Through, Momentary + # Cessation Mode) + opModHVRTMomentaryCessation = 13 + # 14 - opModHVRTMustTrip (High Voltage Ride Through, Must Trip Mode) + opModHVRTMustTrip = 14 + # 15 - opModLFRTMayTrip (Low Frequency Ride Through) + opModLFRTMayTrip = 15 diff --git a/src/python/otsim/ieee_2030_5/client_helper/utils/__init__.py b/src/python/otsim/ieee_2030_5/client_helper/utils/__init__.py new file mode 100644 index 0000000..1fdeb8b --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/utils/__init__.py @@ -0,0 +1,209 @@ +import base64 +import uuid +from dataclasses import dataclass, fields +from pathlib import Path +from typing import Optional, Type + +from xsdata.formats.dataclass.context import XmlContext +from xsdata.formats.dataclass.parsers.config import ParserConfig +from xsdata.formats.dataclass.parsers.xml import XmlParser +from xsdata.formats.dataclass.serializers import XmlSerializer +from xsdata.formats.dataclass.serializers.config import SerializerConfig +from xsdata.formats.dataclass.parsers.handlers import XmlEventHandler + +# SEP 2.0 model package is available — xsdata context auto-discovers all +# annotated dataclasses in client_helper.models.sep at parse time. +from .. import models # noqa: F401 — registers all SEP types with XmlContext + +__xml_context__ = XmlContext() +__parser_config__ = ParserConfig(fail_on_unknown_attributes=True, fail_on_unknown_properties=True) +__xml_parser__ = XmlParser(config=__parser_config__, + context=__xml_context__, + handler=XmlEventHandler) +__config__ = SerializerConfig(xml_declaration=False, pretty_print=True) +__serializer__ = XmlSerializer(config=__config__) +__ns_map__ = {None: "urn:ieee:std:2030.5:ns"} + +from .. import types_ as t + + +class PrivateKeyDeosntExist(Exception): + + def __init__(self, private_key_path: Path): + super().__init__() + self.pk_path = private_key_path + + def __str__(self) -> str: + return f"The path {self.pk_path} does not exist!" + + +class CertExistsError(Exception): + + def __init__(self, cert_path: Path): + super().__init__() + self.cert_path = cert_path + + def __str__(self) -> str: + return f"The path {self.cert_path} already exists!" + + +class CADoesNotExist(Exception): + + def __str__(self) -> str: + return "The CA certificate does not exist!" + + +def serialize_dataclass(obj: dataclass) -> str: + """ + Serializes a dataclass that was created via xsdata to an xml string for + returning to a client. + """ + return __serializer__.render(obj, ns_map=__ns_map__) + + +def xml_to_dataclass(xml: str, type: Optional[Type] = None) -> dataclass: + """ + Parse the xml passed and return result from loaded classes. + """ + parsed = __xml_parser__.from_string(xml, type) + return parsed + + +def dataclass_to_xml(dc: dataclass) -> str: + return serialize_dataclass(dc) + + +def get_lfdi_from_cert(path: Path) -> t.Lfdi: + """ + Using the fingerprint of the certifcate return the left truncation of 160 bits with no check digit. + Example: + From: + 3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5-349E-2AD7-4567-2ED1-45EE-213A + Return: + 3E4F-45AB-31ED-FE5B-67E3-43E5-E456-2E31-984E-23E5 + as an integer. + """ + + # 160 / 4 == 40 + fp = OpensslWrapper.tls_get_fingerprint_from_cert(path) + fp = fp.replace(":", "") + lfdi = t.Lfdi(fp[:40]) + return lfdi + + +def get_sfdi_from_lfdi(lfdi: t.Lfdi) -> int: + """ + + Args: + lfdi: + + Returns: + + """ + from ..certs import sfdi_from_lfdi + return sfdi_from_lfdi(lfdi) + + +def uuid_2030_5() -> str: + return str(uuid.uuid4()).replace('-', '').upper() + + +class TLSWrap: + + @staticmethod + def tls_create_private_key(file_path: Path): + """ + Creates a private key in the path that is specified. The path will be overwritten + if it already exists. + + Args: + file_path: + + Returns: + + """ + raise NotImplementedError() + + @staticmethod + def tls_create_ca_certificate(common_name: str, private_key_file: Path, ca_cert_file: Path): + """ + Create a ca certificate from using common name private key and ca certificate file. + + Args: + common_name: + private_key_file: + ca_cert_file: + + Returns: + + """ + raise NotImplementedError() + + @staticmethod + def tls_create_csr(common_name: str, private_key_file: Path, server_csr_file: Path): + """ + + Args: + common_name: + private_key_file: + server_csr_file: + + Returns: + + """ + raise NotImplementedError() + + @staticmethod + def tls_create_signed_certificate(common_name: str, + ca_key_file: Path, + ca_cert_file: Path, + private_key_file: Path, + cert_file: Path, + as_server: bool = False): + """ + + Args: + common_name: + ca_key_file: + ca_cert_file: + private_key_file: + cert_file: + as_server: + + Returns: + + """ + raise NotImplementedError() + + @staticmethod + def tls_get_fingerprint_from_cert(cert_file: Path, algorithm: str = "sha256") -> str: + """ + + Args: + cert_file: + algorithm: + + Returns: + + """ + + @staticmethod + def tls_create_pkcs23_pem_and_cert(private_key_file: Path, cert_file: Path, + combined_file: Path): + """ + + Args: + private_key_file: + cert_file: + combined_file: + + Returns: + + """ + raise NotImplementedError() + + +from .tls_wrapper import OpensslWrapper +from .cryptography_wrapper import CryptographyWrapper + +__all__ = ['OpensslWrapper', 'CryptographyWrapper', 'uuid_2030_5'] diff --git a/src/python/otsim/ieee_2030_5/client_helper/utils/cryptography_wrapper.py b/src/python/otsim/ieee_2030_5/client_helper/utils/cryptography_wrapper.py new file mode 100644 index 0000000..3543de8 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/utils/cryptography_wrapper.py @@ -0,0 +1,250 @@ +import datetime +from pathlib import Path +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography import x509 +from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID + + +from . import CADoesNotExist, CertExistsError, PrivateKeyDeosntExist, TLSWrap + +class CryptographyWrapper(TLSWrap): + @staticmethod + def tls_create_private_key(file_path: Path) -> bool: + """ + Creates a private key in the path that is specified. The path will be overwritten + if it already exists. + + Args: + file_path: + + Returns: + + """ + pk = ec.generate_private_key(ec.SECP224R1(), default_backend()) + result = pk.private_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption()) + file_path.parent.mkdir(parents=True, exist_ok=True) + file_path.open("wb").write(result) + return True + + + @staticmethod + def tls_create_ca_certificate(common_name: str, private_key_file: Path, ca_cert_file: Path): + """ + Create a ca certificate from using common name private key and ca certificate file. + + Args: + common_name: + private_key_file: + ca_cert_file: + + Returns: + + """ + if ca_cert_file.exists(): + raise CertExistsError(ca_cert_file) + + if not private_key_file.exists(): + CryptographyWrapper.tls_create_private_key(private_key_file) + + pk = serialization.load_pem_private_key( + private_key_file.read_bytes(), None, default_backend()) + + # Create CSR for the CA Cetificate + # csr = x509.CertificateSigningRequestBuilder().subject_name( + # x509.Name([ + # x509.NameAttribute(NameOID.COMMON_NAME, "CA") + # # Provide various details about who we are. + # # x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), + # # x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"), + # # x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"), + # # x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"), + # # x509.NameAttribute(NameOID.COMMON_NAME, common_name), + # ])).add_extension( + # x509.SubjectAlternativeName([ + # # Describe what sites we want this certificate for. + # # x509.DNSName(u"mysite.com"), + # # x509.DNSName(u"www.mysite.com"), + # x509.DNSName(common_name), + # ]), + # critical=False, + # # Sign the CSR with our private key. + # ).sign(pk, hashes.SHA256()) + + ca_subject = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, common_name) + ]) + + # # Various details about who we are. For a self-signed certificate the + # # subject and issuer are always the same. + # subject = issuer = x509.Name([ + # x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"), + # x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"), + # x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"), + # x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"My Company"), + # x509.NameAttribute(NameOID.COMMON_NAME, u"mysite.com"), + # ]) + + cert = x509.CertificateBuilder().subject_name( + ca_subject + ).issuer_name( + ca_subject + ).public_key( + pk.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.now(datetime.timezone.utc) + ).not_valid_after( + # Our certificate will be valid for 10 days + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=20*365) + ).add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=True + # Sign our certificate with our private key + ).sign(pk, hashes.SHA256()) + + ca_cert_file.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) + + @staticmethod + def tls_create_csr(common_name: str, private_key_file: Path, server_csr_file: Path): + """ + + Args: + common_name: + private_key_file: + server_csr_file: + + Returns: + + """ + csr = x509.CertificateSigningRequest().subject_name(x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, common_name) + ])).add_extention( + x509.SubjectAlternativeName([ + x509.DNSName(common_name) + ]), + critical=True + ).sign(key, hashes.SHA256()) + + @staticmethod + def tls_create_device_certificate(ipaddress: str, + ca_key_file: Path, + ca_cert_file: Path, + private_key_file: Path, + cert_file: Path): + pass + + @staticmethod + def tls_create_signed_certificate(common_name: str, + ca_key_file: Path, + ca_cert_file: Path, + private_key_file: Path, + cert_file: Path, + as_server: bool = False): + """ + + Args: + common_name: + ca_key_file: + ca_cert_file: + private_key_file: + cert_file: + as_server: + + Returns: + + """ + + if not ca_key_file.exists() or not ca_cert_file.exists(): + raise CADoesNotExist() + + if not private_key_file.exists(): + raise PrivateKeyDeosntExist(private_key_file) + + if cert_file.exists(): + raise CertExistsError(cert_file) + + pk = serialization.load_pem_private_key( + private_key_file.read_bytes(), None, default_backend()) + + signing_key = serialization.load_pem_private_key( + ca_key_file.read_bytes(), None, default_backend()) + + signing_cert = x509.load_pem_x509_certificate( + ca_cert_file.read_bytes(), default_backend() + ) + + san = x509.SubjectAlternativeName([x509.DNSName(common_name)]) + + builder = x509.CertificateBuilder().subject_name( + x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "")]) + ).issuer_name( + signing_cert.subject + ).public_key( + pk.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.datetime.now(datetime.timezone.utc) + ).not_valid_after( + # Our certificate will be valid for 10 days + datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=20*365) + ).add_extension( + san, False + # Sign our certificate with our private key + ) + + # builder = builder.add_extension( + # x509.KeyUsage(digital_signature=True, key_encipherment=True, + # content_commitment=False, + # data_encipherment=False, key_agreement=False, + # key_cert_sign=False, + # crl_sign=False, + # encipher_only=False, decipher_only=False + # ), + # critical=True) + + if as_server: + builder = builder.add_extension( + x509.ExtendedKeyUsage((ExtendedKeyUsageOID.SERVER_AUTH,)), + critical=False + ) + cert = builder.sign(signing_key, hashes.SHA256()) + cert_file.write_bytes( + cert.public_bytes(serialization.Encoding.PEM) + ) + + @staticmethod + def tls_get_fingerprint_from_cert(cert_file: Path, algorithm: str = "sha256"): + """ + + Args: + cert_file: + algorithm: + + Returns: + + """ + cert = x509.load_pem_x509_certificate(cert_file.read_bytes(), default_backend()) + results = cert.fingerprint(hashes.SHA256()) + return results.hex(":") + + @staticmethod + def tls_create_pkcs23_pem_and_cert(private_key_file: Path, cert_file: Path, + combined_file: Path): + """ + + Args: + private_key_file: + cert_file: + combined_file: + + Returns: + + """ + with combined_file.open("wb") as fp: + fp.write(private_key_file.read_bytes() + b"\n" + + cert_file.read_bytes() + b"\n") \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/client_helper/utils/tls_wrapper.py b/src/python/otsim/ieee_2030_5/client_helper/utils/tls_wrapper.py new file mode 100644 index 0000000..f84afc8 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/client_helper/utils/tls_wrapper.py @@ -0,0 +1,145 @@ +import subprocess +from pathlib import Path +from . import TLSWrap +import logging + +_log = logging.getLogger(__name__) + +class OpensslWrapper(TLSWrap): + opensslcnf: Path = None + + def __init__(self, opensslconf: Path): + OpensslWrapper.opensslcnf = opensslconf + + @staticmethod + def __set_cnf_from_cert_path___(path: Path): + if OpensslWrapper.opensslcnf is None: + check_path = path.parent.parent.joinpath("openssl.cnf") + if check_path.exists(): + OpensslWrapper.opensslcnf = check_path + + @staticmethod + def tls_create_private_key(file_path: Path): + OpensslWrapper.__set_cnf_from_cert_path___(file_path) + # openssl ecparam -out private/ec-cakey.pem -name prime256v1 -genkey + cmd = ["openssl", "ecparam", "-out", str(file_path), "-name", "prime256v1", "-genkey"] + return subprocess.check_output(cmd, text=True) + + @staticmethod + def tls_create_ca_certificate(common_name: str, private_key_file: Path, ca_cert_file: Path): + OpensslWrapper.__set_cnf_from_cert_path___(ca_cert_file) + # openssl req -new -x509 -days 3650 -config openssl.cnf \ + # -extensions v3_ca -key private/ec-cakey.pem -out certs/ec-cacert.pem + cmd = [ + "openssl", "req", "-new", "-x509", "-days", "3650", "-subj", f"/C=US/CN={common_name}", + "-config", + str(OpensslWrapper.opensslcnf), "-extensions", "v3_ca", "-key", + str(private_key_file), "-out", + str(ca_cert_file) + ] + _log.debug(" ".join(cmd)) + return subprocess.check_output(cmd, text=True) + + @staticmethod + def tls_create_csr(common_name: str, private_key_file: Path, server_csr_file: Path): + OpensslWrapper.__set_cnf_from_cert_path___(private_key_file) + subject_name = common_name.split(":")[0] + # openssl req -new -key server.key -out server.csr -sha256 + cmd = [ + "openssl", "req", "-new", "-config", + str(OpensslWrapper.opensslcnf), "-subj", f"/C=US/CN={subject_name}", "-key", + str(private_key_file), "-out", + str(server_csr_file), "-sha256" + ] + return subprocess.check_output(cmd, text=True) + + @staticmethod + def tls_create_signed_certificate(common_name: str, + ca_key_file: Path, + ca_cert_file: Path, + private_key_file: Path, + cert_file: Path, + as_server: bool = False): + OpensslWrapper.__set_cnf_from_cert_path___(cert_file) + subject_name = common_name.split(":")[0] + csr_file = Path(f"/tmp/{common_name}") + OpensslWrapper.tls_create_csr(common_name, private_key_file, csr_file) + # openssl ca -keyfile /root/tls/private/ec-cakey.pem -cert /root/tls/certs/ec-cacert.pem \ + # -in server.csr -out server.crt -config /root/tls/openssl.cnf + cmd = [ + "openssl", + "ca", + "-keyfile", + str(ca_key_file), + "-cert", + str(ca_cert_file), + "-subj", + f"/C=US/CN={subject_name}", + "-in", + str(csr_file), + "-out", + str(cert_file), + "-config", + str(OpensslWrapper.opensslcnf), + # For no prompt use -batch + "-batch" + ] + # if as_server: + # "-server" + print(" ".join(cmd)) + ret_value = subprocess.check_output(cmd, text=True) + csr_file.unlink() + return ret_value + + @staticmethod + def tls_get_fingerprint_from_cert(cert_file: Path, algorithm: str = "sha256") -> str: + OpensslWrapper.__set_cnf_from_cert_path___(cert_file) + if algorithm == "sha256": + algorithm = "-sha256" + else: + raise NotImplementedError() + + cmd = ["openssl", "x509", "-in", str(cert_file), "-noout", "-fingerprint", algorithm] + ret_value = subprocess.check_output(cmd, text=True) + if "=" in ret_value: + ret_value = ret_value.split("=")[1].strip() + return ret_value + + @staticmethod + def tls_create_pkcs23_pem_and_cert(private_key_file: Path, cert_file: Path, + combined_file: Path): + OpensslWrapper.__set_cnf_from_cert_path___(cert_file) + # openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out cert-and-key.pfx + tmpfile = Path("/tmp/tmp.p12") + tmpfile2 = Path("/tmp/all.pem") + tmpfile.unlink(missing_ok=True) + cmd = [ + "openssl", "pkcs12", "-export", "-in", + str(cert_file), "-inkey", + str(private_key_file), "-out", + str(tmpfile), "-passout", "pass:" + ] + subprocess.check_output(cmd, text=True) + + # openssl pkcs12 -in path.p12 -out newfile.pem -nodes + cmd = [ + "openssl", "pkcs12", "-in", + str(tmpfile), "-out", + str(tmpfile2), "-nodes", "-passin", "pass:" + ] + # cmd = ["openssl", "pkcs12", "-in", str(tmpfile), + # "-out", str(combined_file), "-clcerts", "-nokeys", "-passin", "pass:"] + # -clcerts -nokeys + subprocess.check_output(cmd, text=True) + + with open(combined_file, "w") as fp: + in_between = False + for line in tmpfile2.read_text().split("\n"): + if not in_between: + if "BEGIN" in line: + fp.write(f"{line}\n") + in_between = True + else: + fp.write(f"{line}\n") + if "END" in line: + in_between = False diff --git a/src/python/otsim/ieee_2030_5/constants.py b/src/python/otsim/ieee_2030_5/constants.py new file mode 100644 index 0000000..da848f3 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/constants.py @@ -0,0 +1,79 @@ +from .client_helper.models.constants import ( + AccumlationBehaviourType, CommodityType, FlowDirectionType, + KindType, UomType +) +from .client_helper.models import ReadingType + +class TypeConstants(object): + + ACTIVE_POWER = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Forward, + kind=KindType.Power, + uom=UomType.W, + powerOfTenMultiplier=0, + ) + + REACTIVE_POWER = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Forward, + kind=KindType.Power, + uom=UomType.VAr, + powerOfTenMultiplier=0, + ) + + APPARENT_POWER = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Forward, + kind=KindType.Power, + uom=UomType.VA, + powerOfTenMultiplier=0, + ) + + VOLTAGE = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Not_applicable, + kind=KindType.Not_applicable, + uom=UomType.Voltage, + powerOfTenMultiplier=0, + ) + + CURRENT = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Forward, + kind=KindType.Not_applicable, + uom=UomType.Amperes, + powerOfTenMultiplier=-3, + ) + + FREQUENCY = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Not_applicable, + kind=KindType.Not_applicable, + uom=UomType.Hz, + powerOfTenMultiplier=-2, + ) + + ENERGY_EXPORTED = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Summation, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Forward, + kind=KindType.Energy, + uom=UomType.Wh, + powerOfTenMultiplier=0, + ) + + PERCENTAGE = ReadingType( + accumulationBehaviour=AccumlationBehaviourType.Instantaneous, + commodity=CommodityType.Electricity_secondary_metered, + flowDirection=FlowDirectionType.Not_applicable, + kind=KindType.Not_applicable, + uom=UomType.Not_applicable, + powerOfTenMultiplier=2, + ) \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/msgbus/__init__.py b/src/python/otsim/ieee_2030_5/msgbus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/python/otsim/ieee_2030_5/msgbus/envelope.py b/src/python/otsim/ieee_2030_5/msgbus/envelope.py new file mode 100644 index 0000000..8a43192 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/msgbus/envelope.py @@ -0,0 +1,107 @@ +import enum, typing + +class EnvelopeKind(enum.Enum): + STATUS = 'Status' + UPDATE = 'Update' + CONFIRMATION = 'Confirmation' + METRIC = 'Metric' + +class MetricKind(enum.Enum): + COUNTER = 'Counter' + GAUGE = 'Gauge' + +class Point(typing.TypedDict): + tag: str + value: float + ts: int + +class Status(typing.TypedDict): + measurements: typing.List[Point] + +class Update(typing.TypedDict): + updates: typing.List[Point] + recipient: str + confirm: str + +class Confirmation(typing.TypedDict): + confirm: str + errors: typing.Dict[str, str] + +class Metric(typing.TypedDict): + kind: str + name: str + desc: str + value: float + +class Metrics(typing.TypedDict): + metrics: typing.List[Metric] + +class Envelope(typing.TypedDict): + version: str + kind: EnvelopeKind + metadata: typing.Dict[str, str] + contents: str + +def new_status_envelope(sender: str, status: Status) -> Envelope: + env: Envelope = { + 'version': 'v1', + 'kind': EnvelopeKind.STATUS.value, + 'metadata': {'sender': sender}, + 'contents': status, + } + + return env + +def new_update_envelope(sender: str, update: Update) -> Envelope: + if 'recipient' not in update: + update['recipient'] = '' + + if 'confirm' not in update: + update['confirm'] = '' + + env: Envelope = { + 'version': 'v1', + 'kind': EnvelopeKind.UPDATE.value, + 'metadata': {'sender': sender}, + 'contents': update, + } + + return env + +def new_confirmation_envelope(sender: str, conf: Confirmation) -> Envelope: + env: Envelope = { + 'version': 'v1', + 'kind': EnvelopeKind.CONFIRMATION.value, + 'metadata': {'sender': sender}, + 'contents': conf, + } + + return env + +def new_metric_envelope(sender: str, metrics: Metrics) -> Envelope: + env: Envelope = { + 'version': 'v1', + 'kind': EnvelopeKind.METRIC.value, + 'metadata': {'sender': sender}, + 'contents': metrics, + } + + return env + +def status_from_envelope(env: Envelope) -> Status: + if env['kind'] != EnvelopeKind.STATUS.value: + return None + + return env['contents'] + +def update_from_envelope(env: Envelope) -> Update: + if env['kind'] != EnvelopeKind.UPDATE.value: + return None + + return env['contents'] + +def confirmation_from_envelope(env: Envelope) -> Confirmation: + if env['kind'] != EnvelopeKind.CONFIRMATION.value: + return None + + return env['contents'] \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/msgbus/metrics.py b/src/python/otsim/ieee_2030_5/msgbus/metrics.py new file mode 100644 index 0000000..66a0b7c --- /dev/null +++ b/src/python/otsim/ieee_2030_5/msgbus/metrics.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import json, threading, time, typing + +import otsim.msgbus.envelope as envelope + +from otsim.msgbus.envelope import Metric, MetricKind +from otsim.msgbus.pusher import Pusher + +class MetricsPusher: + def __init__(self: MetricsPusher): + self.running = False + self.metrics: typing.Dict[str, Metric] = {} + + def start(self: MetricsPusher, pusher: Pusher, name: str) -> None: + self.running = True + + self.thread = threading.Thread(target=self.__run, args=(pusher, name,)) + self.thread.start() + + def stop(self: MetricsPusher) -> None: + self.running = False + self.thread.join() + + def new_metric(self: MetricsPusher, kind: MetricKind, name: str, desc: str) -> None: + self.metrics[name] = {'kind': kind.value, 'name': name, 'desc': desc, 'value': 0.0} + + def incr_metric(self: MetricsPusher, name: str) -> None: + if name in self.metrics: + metric = self.metrics[name] + metric['value'] += 1.0 + self.metrics[name] = metric + + def incr_metric_by(self: MetricsPusher, name: str, val: int) -> None: + if name in self.metrics: + metric = self.metrics[name] + metric['value'] += float(val) + self.metrics[name] = metric + + def set_metric(self: MetricsPusher, name: str, val: float) -> None: + if name in self.metrics: + metric = self.metrics[name] + metric['value'] = val + self.metrics[name] = metric + + def __run(self: MetricsPusher, pusher: Pusher, name: str) -> None: + prefix = name + "_" + + while self.running: + updates: typing.List[Metric] = [] + + for metric in self.metrics.values(): + copy = metric + + if not copy['name'].startswith(prefix): + copy['name'] = prefix + copy['name'] + + updates.append(copy) + + if len(updates) > 0: + env = envelope.new_metric_envelope(name, {'metrics': updates}) + pusher.push('HEALTH', env) + + time.sleep(5) \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/msgbus/pusher.py b/src/python/otsim/ieee_2030_5/msgbus/pusher.py new file mode 100644 index 0000000..5c14050 --- /dev/null +++ b/src/python/otsim/ieee_2030_5/msgbus/pusher.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import json, zmq + +from msgbus.envelope import Envelope + +class Pusher: + def __init__(self: Pusher, endpoint: str): + self.ctx = zmq.Context() + self.socket = self.ctx.socket(zmq.PUSH) + + self.socket.connect(endpoint) + self.socket.setsockopt(zmq.LINGER, 0) + + def push(self: Pusher, topic: str, env: Envelope) -> None: + self.socket.send_multipart((topic.encode(), json.dumps(env).encode())) \ No newline at end of file diff --git a/src/python/otsim/ieee_2030_5/msgbus/subscriber.py b/src/python/otsim/ieee_2030_5/msgbus/subscriber.py new file mode 100644 index 0000000..5437f1d --- /dev/null +++ b/src/python/otsim/ieee_2030_5/msgbus/subscriber.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import json, threading, typing, zmq + +from msgbus.envelope import Envelope + +status_handler = typing.Callable[[Envelope], None] +update_handler = typing.Callable[[Envelope], None] + +class Subscriber: + def __init__(self: Subscriber, endpoint: str): + self.status_handlers: typing.List[status_handler] = [] + self.update_handlers: typing.List[update_handler] = [] + + self.running = False + + self.ctx = zmq.Context() + self.socket = self.ctx.socket(zmq.SUB) + + self.socket.connect(endpoint) + self.socket.setsockopt(zmq.LINGER, 0) + + def add_status_handler(self: Subscriber, handler: status_handler) -> None: + self.status_handlers.append(handler) + + def add_update_handler(self: Subscriber, handler: update_handler) -> None: + self.update_handlers.append(handler) + + def start(self: Subscriber, topic: str) -> None: + self.running = True + + self.thread = threading.Thread(target=self.__run, args=(topic,)) + self.thread.start() + + def stop(self: Subscriber) -> None: + self.running = False + + self.socket.close() + self.ctx.term() + + self.thread.join() + + def __run(self: Subscriber, topic: str) -> None: + self.socket.setsockopt(zmq.SUBSCRIBE, topic.encode()) + + while self.running: + data = self.socket.recv_multipart() + + # this should never happen... + if data[0].decode() != topic: + continue + + env = json.loads(data[1]) + + if env['kind'] == 'Status': + for handler in self.status_handlers: + handler(env) + elif env['kind'] == 'Update': + for handler in self.update_handlers: + handler(env) \ No newline at end of file diff --git a/src/python/setup.py b/src/python/setup.py index fd5cae7..4a67370 100644 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -11,6 +11,9 @@ 'pyzmq', 'requests', 'windpowerlib', + 'werkzeug', + 'xsdata', + 'cryptography' ] SCRIPTS = [ @@ -18,6 +21,7 @@ 'ot-sim-io-module = otsim.io.io:main', 'ot-sim-wind-turbine-anemometer-module = otsim.wind_turbine.anemometer.anemometer:main', 'ot-sim-wind-turbine-power-output-module = otsim.wind_turbine.power_output.power_output:main', + 'ot-sim-ieee-20305-client-module = otsim.ieee_2030_5.client:main', ] if platform.machine() == 'arm64':