diff --git a/pyproject.toml b/pyproject.toml index 2be57efc..7556fc5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ {name = "Max Chesterfield", email = "max.chesterfield@zepben.com"} ] dependencies = [ - "zepben.protobuf==1.0.0", + "zepben.protobuf==1.1.0", "typing_extensions==4.14.1", "requests==2.32.5", "urllib3==2.5.0", diff --git a/src/zepben/ewb/database/sql/column.py b/src/zepben/ewb/database/sql/column.py index 43a0e50f..2b3ea31e 100644 --- a/src/zepben/ewb/database/sql/column.py +++ b/src/zepben/ewb/database/sql/column.py @@ -7,7 +7,7 @@ from enum import Enum -from zepben.ewb.dataclassy import dataclass +from dataclasses import dataclass from zepben.ewb.util import require @@ -21,17 +21,33 @@ def sql(self): return self.value +class Type(Enum): + STRING = "TEXT" + INTEGER = "INTEGER" + DOUBLE = "NUMBER" + BOOLEAN = "BOOLEAN" + UUID = "TEXT" + TIMESTAMP = "TEXT" + BYTES = "BLOB" + + @dataclass(slots=True) class Column: query_index: int name: str - type: str + type: str | Type + """Deprecated, use `type` instead""" nullable: Nullable = Nullable.NONE - def __init__(self): + def __post_init__(self): require(self.query_index >= 0, lambda: "You cannot use a negative query index.") - require(not self.name.isspace() and self.name, lambda: "Column Name cannot be blank.") - require(not self.type.isspace() and self.type, lambda: "Column Type cannot be blank.") + if not isinstance(self.type, Type): + DeprecationWarning("Passing strings directly to Column is being phased out, use the Type enum instead.") + require(not self.name.isspace() and self.name, lambda: "Column Name cannot be blank.") + require(not self.type.isspace() and self.type, lambda: "Column Type cannot be blank.") + # FIXME: We should accept isinstance(self.type, Type) from here on. def __str__(self): + if isinstance(self.type, Type): + return f"{self.name} {self.type.value} {self.nullable.sql}".rstrip() return f"{self.name} {self.type} {self.nullable.sql}".rstrip() diff --git a/src/zepben/ewb/database/sql/sql_table.py b/src/zepben/ewb/database/sql/sql_table.py index f94990ff..e60c538e 100644 --- a/src/zepben/ewb/database/sql/sql_table.py +++ b/src/zepben/ewb/database/sql/sql_table.py @@ -8,7 +8,8 @@ from abc import abstractmethod, ABCMeta from operator import attrgetter -from typing import List, Optional, Type, Any, Generator +from typing import List, Optional, Any, Generator +from zepben.ewb.database.sql.column import Type from zepben.ewb.database.sql.column import Column, Nullable @@ -136,7 +137,7 @@ def _build_column_set(clazz: Type[Any], instance: SqlTable) -> Generator[Column, yield from sorted(cols, key=attrgetter('query_index')) - def _create_column(self, name: str, type_: str, nullable: Nullable = Nullable.NONE) -> Column: + def _create_column(self, name: str, type_: str | Type, nullable: Nullable = Nullable.NONE) -> Column: self.column_index += 1 # noinspection PyArgumentList return Column(self.column_index, name, type_, nullable) diff --git a/src/zepben/ewb/database/sqlite/common/base_collection_reader.py b/src/zepben/ewb/database/sqlite/common/base_collection_reader.py index e344bf1e..a51b3d8a 100644 --- a/src/zepben/ewb/database/sqlite/common/base_collection_reader.py +++ b/src/zepben/ewb/database/sqlite/common/base_collection_reader.py @@ -70,7 +70,7 @@ def set_identifier(identifier: str) -> str: count = 0 while results.next(): if process_row(table, results, set_identifier): - count = count + 1 + count += 1 return count except SqlException as e: diff --git a/src/zepben/ewb/database/sqlite/customer/customer_cim_reader.py b/src/zepben/ewb/database/sqlite/customer/customer_cim_reader.py index 879b0cc1..8156f991 100644 --- a/src/zepben/ewb/database/sqlite/customer/customer_cim_reader.py +++ b/src/zepben/ewb/database/sqlite/customer/customer_cim_reader.py @@ -22,6 +22,7 @@ from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.model.cim.iec61968.customers.pricing_structure import PricingStructure from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff +from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval from zepben.ewb.services.customer.customers import CustomerService class CustomerCimReader(BaseCimReader): @@ -40,6 +41,12 @@ def __init__(self, service: CustomerService): ################### def _load_agreement(self, agreement: Agreement, table: TableAgreements, result_set: ResultSet) -> bool: + start = result_set.get_instant(table.validity_interval_start.query_index, on_none=None) + end = result_set.get_instant(table.validity_interval_end.query_index, on_none=None) + + if start is not None or end is not None: + agreement.validity_interval = DateTimeInterval(start=start, end=end) + return self._load_document(agreement, table, result_set) ###################### diff --git a/src/zepben/ewb/database/sqlite/customer/customer_cim_writer.py b/src/zepben/ewb/database/sqlite/customer/customer_cim_writer.py index 926531ab..badadcac 100644 --- a/src/zepben/ewb/database/sqlite/customer/customer_cim_writer.py +++ b/src/zepben/ewb/database/sqlite/customer/customer_cim_writer.py @@ -38,6 +38,8 @@ def __init__(self, database_tables: CustomerDatabaseTables): ################### def _save_agreement(self, table: TableAgreements, insert: PreparedStatement, agreement: Agreement, description: str) -> bool: + insert.add_value(table.validity_interval_start.query_index, getattr(agreement.validity_interval, 'start', None)) + insert.add_value(table.validity_interval_end.query_index, getattr(agreement.validity_interval, 'end', None)) return self._save_document(table, insert, agreement, description) ###################### diff --git a/src/zepben/ewb/database/sqlite/extensions/result_set.py b/src/zepben/ewb/database/sqlite/extensions/result_set.py index 5a84afb2..7a35eeb5 100644 --- a/src/zepben/ewb/database/sqlite/extensions/result_set.py +++ b/src/zepben/ewb/database/sqlite/extensions/result_set.py @@ -55,7 +55,7 @@ def get_string(self, column_index: int, on_none: Union[Optional[str], Type[Excep value = self._current_row[column_index - 1] if value is None: return self._value_or_raise(on_none) - elif isinstance(value, str): + elif isinstance(value, str) or on_none is None: # FIXME: TESTING HAX!! this shouldnt be in the PR return value else: raise ValueError diff --git a/src/zepben/ewb/database/sqlite/network/network_cim_reader.py b/src/zepben/ewb/database/sqlite/network/network_cim_reader.py index 4cb9ba3b..4ad56092 100644 --- a/src/zepben/ewb/database/sqlite/network/network_cim_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_cim_reader.py @@ -12,10 +12,19 @@ from zepben.ewb.database.sqlite.tables.associations.table_end_devices_end_device_functions import TableEndDevicesEndDeviceFunctions from zepben.ewb.database.sqlite.tables.associations.table_synchronous_machines_reactive_capability_curves import \ TableSynchronousMachinesReactiveCapabilityCurves +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details import TableContactDetails + +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_electronic_addresses import TableContactDetailsElectronicAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_street_addresses import TableContactDetailsStreetAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_telephone_numbers import TableContactDetailsTelephoneNumbers from zepben.ewb.database.sqlite.tables.extensions.iec61968.metering.table_pan_demand_response_functions import TablePanDemandResponseFunctions +from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_directional_current_relay import TableDirectionalCurrentRelay from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.wires.table_battery_controls import TableBatteryControls from zepben.ewb.database.sqlite.tables.iec61968.assets.table_asset_functions import TableAssetFunctions +from zepben.ewb.database.sqlite.tables.iec61968.common.table_electronic_addresses import TableElectronicAddresses +from zepben.ewb.database.sqlite.tables.iec61968.common.table_telephone_numbers import TableTelephoneNumbers from zepben.ewb.database.sqlite.tables.iec61968.metering.table_end_device_functions import TableEndDeviceFunctions +from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points_contact_details import TableUsagePointsContactDetails from zepben.ewb.database.sqlite.tables.iec61970.base.core.table_curve_data import TableCurveData from zepben.ewb.database.sqlite.tables.iec61970.base.core.table_curves import TableCurves from zepben.ewb.database.sqlite.tables.iec61970.base.wires.table_earth_fault_compensators import TableEarthFaultCompensators @@ -27,10 +36,16 @@ from zepben.ewb.database.sqlite.tables.iec61970.base.wires.table_rotating_machines import TableRotatingMachines from zepben.ewb.database.sqlite.tables.iec61970.base.wires.table_static_var_compensator import TableStaticVarCompensators from zepben.ewb.database.sqlite.tables.iec61970.base.wires.table_synchronous_machines import TableSynchronousMachines +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails +from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.iec61968.metering.pan_demand_reponse_function import PanDemandResponseFunction +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay +from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61968.assets.asset_function import AssetFunction +from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress +from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61968.metering.end_device_function_kind import EndDeviceFunctionKind from zepben.ewb.model.cim.iec61970.base.core.curve import Curve from zepben.ewb.model.cim.iec61970.base.wires.earth_fault_compensator import EarthFaultCompensator @@ -341,6 +356,13 @@ def __init__(self, service: NetworkService): self._service = service """The :class:`NetworkService` used to store any items read from the database.""" + + # + # NOTE: Since `ContactDetails` aren't an `IdentifiedObject`, we can't store them directly in the `NetworkService`. For now, we will + # just keep a local cache during load. In the future we might decide to store them in a public fashion in the service, but this + # is likely to only be when we want to author the `ContactDetails`, or reuse them between places and require a lookup. + # + self._contact_details_by_id = {} ################################## # Extensions IEC61968 Asset Info # @@ -386,6 +408,72 @@ def load_relay_info(self, table: TableRelayInfo, result_set: ResultSet, set_iden return self._load_asset_info(relay_info, table, result_set) and self._add_or_throw(relay_info) + ############################## + # Extensions IEC61968 Common # + ############################## + + def load_contact_details(self, contact_details: ContactDetails, table: TableContactDetails, result_set: ResultSet) -> bool: + contact_details.contact_type = result_set.get_string(table.contact_type.query_index, on_none=None) + contact_details.first_name = result_set.get_string(table.first_name.query_index, on_none=None) + contact_details.last_name = result_set.get_string(table.last_name.query_index, on_none=None) + contact_details.preferred_contact_method = ContactMethodType[result_set.get_string(table.preferred_contact_method.query_index)] + contact_details.is_primary = result_set.get_boolean(table.is_primary.query_index, on_none=None) + contact_details.business_name = result_set.get_string(table.business_name.query_index, on_none=None) + + self._contact_details_by_id[contact_details.id] = contact_details + return True + + def load_contact_details_electronic_addresses(self, table: TableContactDetailsElectronicAddresses, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: + """ + Create an :class:`ElectronicAddress`, populate its fields from :class:`TableContactDetailsElectronicAddresses`, + and add it to the specified :class:`ContactDetails`. + + :param table: The database table to read the :class:`ElectronicAddress` fields from. + :param result_set: The record in the database table containing the fields for this :class:`ElectronicAddress`. + :param set_identifier: A callback to register the mRID of this :class:`ElectronicAddress` for logging purposes. + + :return: True if the :class:`ElectronicAddress` was successfully read from the database and added to the :class:`ContactDetails`. + :raises SqlException: For any errors encountered reading from the database. + """ + contact_details_id = result_set.get_string(table.contact_details_id.query_index, on_none=None) + self._contact_details_by_id.get(contact_details_id) \ + .add_electronic_address(self._load_electronic_address(table, result_set)) + return True + + def load_contact_details_street_addresses(self, table: TableContactDetailsStreetAddresses, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: + """ + Create a :class:`StreetAddress`, populate its fields from :class:`TableContactDetailsStreetAddresses`, + and add it to the specified :class:`ContactDetails`. + + :param table: The database table to read the :class:`StreetAddress` fields from. + :param result_set: The record in the database table containing the fields for this :class:`StreetAddress`. + :param set_identifier: A callback to register the mRID of this :class:`ElectronicAddress` for logging purposes. + + :return: True if the :class:`StreetAddress` was successfully read from the database and added to the :class:`ContactDetails`. + :raises SqlException: For any errors encountered reading from the database. + """ + contact_details_id = result_set.get_string(table.contact_details_id.query_index, on_none=None) + self._contact_details_by_id.get(contact_details_id) \ + .contact_address = self._load_street_address(table, result_set) + return True + + def load_contact_details_telephone_number(self, table: TableContactDetailsTelephoneNumbers, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: + """ + Create an :class:`TelephoneNumber`, populate its fields from :class:`TableContactDetailsTelephoneNumbers`, + and add it to the specified :class:`ContactDetails`. + + :param table: The database table to read the :class:`TelephoneNumber` fields from. + :param result_set: The record in the database table containing the fields for this :class:`TelephoneNumber`. + :param set_identifier: A callback to register the mRID of this :class:`ElectronicAddress` for logging purposes. + + :return: True if the :class:`TelephoneNumber` was successfully read from the database and added to the :class:`ContactDetails`. + :raises SqlException: For any errors encountered reading from the database + """ + contact_details_id = result_set.get_string(table.contact_details_id.query_index, on_none=None) + self._contact_details_by_id.get(contact_details_id) \ + .add_phone_number(self._load_telephone_number(table, result_set)) + return True + ################################ # Extensions IEC61968 Metering # ################################ @@ -489,6 +577,29 @@ def load_ev_charging_unit(self, table: TableEvChargingUnits, result_set: ResultS # Extensions IEC61970 Base Protection # ####################################### + def load_directional_current_relay(self, table: TableDirectionalCurrentRelay, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: + """ + * Create a [DirectionalCurrentRelay] and populate its fields from [TableDirectionalCurrentRelays]. + * + * @param service The [NetworkService] used to store any items read from the database. + * @param table The database table to read the [DirectionalCurrentRelay] fields from. + * @param resultSet The record in the database table containing the fields for this [DirectionalCurrentRelay]. + * @param setIdentifier A callback to register the mRID of this [DirectionalCurrentRelay] for logging purposes. + * + * @return true if the [DirectionalCurrentRelay] was successfully read from the database and added to the service. + * @throws SQLException For any errors encountered reading from the database. + """ + directional_current_relay = DirectionalCurrentRelay(mrid=set_identifier(result_set.get_string(table.mrid.query_index))) + directional_current_relay.directional_characteristic_angle = result_set.get_float(table.directional_characteristic_angle.query_index, on_none=None) + directional_current_relay.polarizing_quantity_type = PolarizingQuantityType[result_set.get_string(table.polarizing_quantity_type.query_index)] + directional_current_relay.relay_element_phase = PhaseCode[result_set.get_string(table.relay_element_phase.query_index)] + directional_current_relay.minimum_pickup_current = result_set.get_float(table.minimum_pickup_current.query_index, on_none=None) + directional_current_relay.current_limit_1 = result_set.get_float(table.current_limit_1.query_index, on_none=None) + directional_current_relay.inverse_time_flag = result_set.get_boolean(table.inverse_time_flag.query_index, on_none=None) + directional_current_relay.time_delay_1 = result_set.get_float(table.time_delay_1.query_index, on_none=None) + + return self._load_protection_relay_function(directional_current_relay, table, result_set) and self._add_or_throw(directional_current_relay) + def load_distance_relay(self, table: TableDistanceRelays, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: """ Create a :class:`DistanceRelay` and populate its fields from :class:`TableDistanceRelays`. @@ -1006,6 +1117,14 @@ def _load_structure(self, structure: Structure, table: TableStructures, result_s # IEC61968 Common # ################### + @staticmethod + def _load_electronic_address(table: TableElectronicAddresses, result_set: ResultSet) -> ElectronicAddress: + return ElectronicAddress( + result_set.get_string(table.email_1.query_index, on_none=None), + result_set.get_boolean(table.is_primary.query_index, on_none=None), + result_set.get_string(table.description.query_index, on_none=None), + ) + def load_location(self, table: TableLocations, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: """ Create a :class:`Location` and populate its fields from :class:`TableLocations`. @@ -1079,25 +1198,41 @@ def _load_street_address(self, table: TableStreetAddresses, result_set: ResultSe @staticmethod def _load_street_detail(table: TableStreetAddresses, result_set: ResultSet) -> Optional[StreetDetail]: sd = StreetDetail( - result_set.get_string(table.building_name.query_index, on_none=None), - result_set.get_string(table.floor_identification.query_index, on_none=None), - result_set.get_string(table.street_name.query_index, on_none=None), - result_set.get_string(table.number.query_index, on_none=None), - result_set.get_string(table.suite_number.query_index, on_none=None), - result_set.get_string(table.type.query_index, on_none=None), - result_set.get_string(table.display_address.query_index, on_none=None) + building_name=result_set.get_string(table.building_name.query_index, on_none=None), + floor_identification=result_set.get_string(table.floor_identification.query_index, on_none=None), + name=result_set.get_string(table.street_name.query_index, on_none=None), + number=result_set.get_string(table.number.query_index, on_none=None), + suite_number=result_set.get_string(table.suite_number.query_index, on_none=None), + type=result_set.get_string(table.type.query_index, on_none=None), + display_address=result_set.get_string(table.display_address.query_index, on_none=None), + building_number=result_set.get_string(table.building_number.query_index, on_none=None) ) - return sd if not sd.all_fields_empty() else None + return sd if not sd.all_fields_null() else None + + @staticmethod + def _load_telephone_number(table: TableTelephoneNumbers, result_set: ResultSet) -> TelephoneNumber: + return TelephoneNumber( + result_set.get_string(table.area_code.query_index, on_none=None), + result_set.get_string(table.city_code.query_index, on_none=None), + result_set.get_string(table.country_code.query_index, on_none=None), + result_set.get_string(table.dial_out.query_index, on_none=None), + result_set.get_string(table.extension.query_index, on_none=None), + result_set.get_string(table.international_prefix.query_index, on_none=None), + result_set.get_string(table.local_number.query_index, on_none=None), + result_set.get_boolean(table.is_primary.query_index, on_none=None), + result_set.get_string(table.description.query_index, on_none=None), + ) @staticmethod def _load_town_detail(table: TableTownDetails, result_set: ResultSet) -> Optional[TownDetail]: td = TownDetail( result_set.get_string(table.town_name.query_index, on_none=None), - result_set.get_string(table.state_or_province.query_index, on_none=None) + result_set.get_string(table.state_or_province.query_index, on_none=None), + result_set.get_string(table.country.query_index, on_none=None), ) - return td if not td.all_fields_null_or_empty() else None + return td if not td.all_fields_null() else None ##################################### # IEC61968 InfIEC61968 InfAssetInfo # @@ -1241,6 +1376,29 @@ def load_usage_point(self, table: TableUsagePoints, result_set: ResultSet, set_i return self._load_identified_object(usage_point, table, result_set) and self._add_or_throw(usage_point) + def load_usage_points_contact_details(self, table: TableUsagePointsContactDetails, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: + """ + Create a :class:`ContactDetails` and populate its fields from :class`TableUsagePointsContactDetails`. + + :param table: The database table to read the :class:`ContactDetails` fields from. + :param result_set: The record in the database table containing the fields for this :class:`ContactDetails` + :param set_identifier: A callback to register the mRID of this :class:`ContactDetails` for logging purposes. + + :return: True if the :class:`ContactDetails` was successfully read from the database and added to the service. + :raises SqlException: For any errors encountered reading from the database. + """ + usage_point_mrid = set_identifier(result_set.get_string(table.usage_point_mrid.query_index)) + contact_details_id = result_set.get_string(table.id.query_index) + set_identifier(f"{usage_point_mrid}-to-{contact_details_id}") + + usage_point = self._ensure_get(usage_point_mrid, UsagePoint) + contact_details = ContactDetails(contact_details_id) + + val = self.load_contact_details(contact_details, table, result_set) + # We add the contact after it has been populated to ensure the `equals` check in the `addContact` works as intended. + usage_point.add_contact(contact_details) + return val + ####################### # IEC61968 Operations # ####################### diff --git a/src/zepben/ewb/database/sqlite/network/network_cim_writer.py b/src/zepben/ewb/database/sqlite/network/network_cim_writer.py index 46f86883..2246da4b 100644 --- a/src/zepben/ewb/database/sqlite/network/network_cim_writer.py +++ b/src/zepben/ewb/database/sqlite/network/network_cim_writer.py @@ -5,8 +5,10 @@ __all__ = ["NetworkCimWriter"] -from typing import Optional +from typing import Optional, Callable, Type, TypeVar +from zepben.ewb import IdentifiedObject +from zepben.ewb.database.sql.sql_table import SqlTable from zepben.ewb.database.sqlite.common.base_cim_writer import BaseCimWriter from zepben.ewb.database.sqlite.extensions.prepared_statement import PreparedStatement from zepben.ewb.database.sqlite.network.network_database_tables import NetworkDatabaseTables @@ -30,11 +32,16 @@ from zepben.ewb.database.sqlite.tables.associations.table_usage_points_end_devices import TableUsagePointsEndDevices from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_reclose_delays import TableRecloseDelays from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_relay_info import TableRelayInfo +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details import TableContactDetails +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_electronic_addresses import TableContactDetailsElectronicAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_street_addresses import TableContactDetailsStreetAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_telephone_numbers import TableContactDetailsTelephoneNumbers from zepben.ewb.database.sqlite.tables.extensions.iec61968.metering.table_pan_demand_response_functions import TablePanDemandResponseFunctions from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.core.table_sites import TableSites from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_loops import TableLoops from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_lv_feeders import TableLvFeeders from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.generation.production.table_ev_charging_units import TableEvChargingUnits +from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_directional_current_relay import TableDirectionalCurrentRelay from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_distance_relays import TableDistanceRelays from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_protection_relay_function_thresholds import \ TableProtectionRelayFunctionThresholds @@ -66,11 +73,13 @@ from zepben.ewb.database.sqlite.tables.iec61968.assets.table_assets import TableAssets from zepben.ewb.database.sqlite.tables.iec61968.assets.table_streetlights import TableStreetlights from zepben.ewb.database.sqlite.tables.iec61968.assets.table_structures import TableStructures +from zepben.ewb.database.sqlite.tables.iec61968.common.table_electronic_addresses import TableElectronicAddresses from zepben.ewb.database.sqlite.tables.iec61968.common.table_location_street_address_field import TableLocationStreetAddressField from zepben.ewb.database.sqlite.tables.iec61968.common.table_location_street_addresses import TableLocationStreetAddresses from zepben.ewb.database.sqlite.tables.iec61968.common.table_locations import TableLocations from zepben.ewb.database.sqlite.tables.iec61968.common.table_position_points import TablePositionPoints from zepben.ewb.database.sqlite.tables.iec61968.common.table_street_addresses import TableStreetAddresses +from zepben.ewb.database.sqlite.tables.iec61968.common.table_telephone_numbers import TableTelephoneNumbers from zepben.ewb.database.sqlite.tables.iec61968.common.table_town_details import TableTownDetails from zepben.ewb.database.sqlite.tables.iec61968.infiec61968.infassetinfo.table_current_transformer_info import TableCurrentTransformerInfo from zepben.ewb.database.sqlite.tables.iec61968.infiec61968.infassetinfo.table_potential_transformer_info import TablePotentialTransformerInfo @@ -79,6 +88,7 @@ from zepben.ewb.database.sqlite.tables.iec61968.metering.table_end_devices import TableEndDevices from zepben.ewb.database.sqlite.tables.iec61968.metering.table_meters import TableMeters from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points import TableUsagePoints +from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points_contact_details import TableUsagePointsContactDetails from zepben.ewb.database.sqlite.tables.iec61968.operations.table_operational_restrictions import TableOperationalRestrictions from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_auxiliary_equipment import TableAuxiliaryEquipment from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_current_transformers import TableCurrentTransformers @@ -167,11 +177,13 @@ from zepben.ewb.database.sqlite.tables.iec61970.base.wires.table_transformer_star_impedances import TableTransformerStarImpedances from zepben.ewb.database.sqlite.tables.iec61970.infiec61970.feeder.table_circuits import TableCircuits from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.extensions.iec61968.metering.pan_demand_reponse_function import PanDemandResponseFunction from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.generation.production.ev_charging_unit import EvChargingUnit +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.distance_relay import DistanceRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme @@ -199,10 +211,12 @@ from zepben.ewb.model.cim.iec61968.assets.asset_owner import AssetOwner from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight from zepben.ewb.model.cim.iec61968.assets.structure import Structure +from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.location import Location from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61968.common.street_detail import StreetDetail +from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61968.common.town_detail import TownDetail from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.current_transformer_info import CurrentTransformerInfo from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.potential_transformer_info import PotentialTransformerInfo @@ -299,6 +313,15 @@ from zepben.ewb.model.cim.iec61970.base.wires.transformer_star_impedance import TransformerStarImpedance from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import Circuit +TSqlTable = TypeVar('TSqlTable', bound=SqlTable) +def db_wrapper(table: Type[TSqlTable]): + def wrapper(func): + def _inner(self, io: IdentifiedObject, *args, **kwargs): + _table: TSqlTable = self._database_tables.get_table(table) + _insert = self._database_tables.get_insert(table) + return func(self, io, *args, table=_table, insert=_insert, **kwargs) + return _inner + return wrapper class NetworkCimWriter(BaseCimWriter): """ @@ -314,7 +337,8 @@ def __init__(self, database_tables: NetworkDatabaseTables): # Extension IEC61968 Asset Info # ################################# - def save_relay_info(self, relay_info: RelayInfo) -> bool: + @db_wrapper(TableRelayInfo) + def save_relay_info(self, relay_info: RelayInfo, table, insert) -> bool: """ Save the :class:`RelayInfo` fields to :class:`TableRelayInfo`. @@ -322,8 +346,6 @@ def save_relay_info(self, relay_info: RelayInfo) -> bool: :return: True if the :class:`RelayInfo` was successfully written to the database, otherwise False. :raises SqlException: For any errors encountered writing to the database. """ - table = self._database_tables.get_table(TableRelayInfo) - insert = self._database_tables.get_insert(TableRelayInfo) reclose_delay_table = self._database_tables.get_table(TableRecloseDelays) reclose_delay_insert = self._database_tables.get_insert(TableRecloseDelays) @@ -338,11 +360,51 @@ def save_relay_info(self, relay_info: RelayInfo) -> bool: return self._save_asset_info(table, insert, relay_info, "relay info") + ############################# + # Extension IEC61968 Common # + ############################# + + def _save_contact_details(self, table: TableContactDetails, insert: PreparedStatement, contact_details: ContactDetails, description: str) -> bool: + insert.add_value(table.id.query_index, contact_details.id) + insert.add_value(table.contact_type.query_index, contact_details.contact_type) + insert.add_value(table.first_name.query_index, contact_details.first_name) + insert.add_value(table.last_name.query_index, contact_details.last_name) + insert.add_value(table.preferred_contact_method.query_index, contact_details.preferred_contact_method.name) + insert.add_value(table.is_primary.query_index, contact_details.is_primary) + insert.add_value(table.business_name.query_index, contact_details.business_name) + + status = self._save_contact_details_street_address(contact_details, contact_details.contact_address) + for it in contact_details.phone_numbers: status = status and self._save_contact_details_telephone_number(contact_details, it) + for it in contact_details.electronic_addresses: status = status and self._save_contact_details_electronic_address(contact_details, it) + + return status and self._try_execute_single_update(insert, description) + + @db_wrapper(TableContactDetailsElectronicAddresses) + def _save_contact_details_electronic_address(self, contact_details: ContactDetails, electronic_address: ElectronicAddress, table, insert) -> bool: + insert.add_value(table.contact_details_id.query_index, contact_details.id) + return self._save_electronic_address(table, insert, electronic_address, f"electronic address for contact {contact_details.id}") + + @db_wrapper(TableContactDetailsStreetAddresses) + def _save_contact_details_street_address(self, contact_details: ContactDetails, street_address: StreetAddress | None, table, insert) -> bool: + if street_address is None: + return True + + insert.add_value(table.contact_details_id.query_index, contact_details.id) + return self._save_street_address(table, insert, street_address, f"street address for contact {contact_details.id}") + + @db_wrapper(TableContactDetailsTelephoneNumbers) + def _save_contact_details_telephone_number(self, contact_details: ContactDetails, phone_number: TelephoneNumber, table, insert) -> bool: + + insert.add_value(table.contact_details_id.query_index, contact_details.id) + + return + ############################### # Extension IEC61968 Metering # ############################### - def save_pan_demand_response_function(self, pan_demand_response_function: PanDemandResponseFunction) -> bool: + @db_wrapper(TablePanDemandResponseFunctions) + def save_pan_demand_response_function(self, pan_demand_response_function: PanDemandResponseFunction, table, insert) -> bool: """ Save the :class:`PanDemandResponseFunction` fields to :class:`TablePanDemandResponseFunctions`. @@ -350,8 +412,6 @@ def save_pan_demand_response_function(self, pan_demand_response_function: PanDem :return: True if the :class:`PanDemandResponseFunction` was successfully written to the database, otherwise False. :raises SqlException: For any errors encountered writing to the database. """ - table = self._database_tables.get_table(TablePanDemandResponseFunctions) - insert = self._database_tables.get_insert(TablePanDemandResponseFunctions) insert.add_value(table.kind.query_index, pan_demand_response_function.kind.short_name) insert.add_value(table.appliance.query_index, pan_demand_response_function._appliance_bitmask) @@ -434,6 +494,27 @@ def save_ev_charging_unit(self, ev_charging_unit: EvChargingUnit) -> bool: # Extension IEC61970 Base Protection # ###################################### + @db_wrapper(TableDirectionalCurrentRelay) + def save_directional_current_relay(self, directional_current_relay: DirectionalCurrentRelay, table, insert) -> bool: + """ + Write the :class:`DirectionalCurrentRelay` fields to :class:`TableDirectionalCurrentRelays`. + + :param directional_current_relay: The :class:`DirectionalCurrentRelay` instance to write to the database. + + :return: True if the :class:`DirectionalCurrentRelay` was successfully written to the database, otherwise false. + :raises SqlException: For any errors encountered writing to the database. + """ + + insert.add_value(table.directional_characteristic_angle.query_index, directional_current_relay.directional_characteristic_angle) + insert.add_value(table.polarizing_quantity_type.query_index, directional_current_relay.polarizing_quantity_type.name) + insert.add_value(table.relay_element_phase.query_index, directional_current_relay.relay_element_phase.name) + insert.add_value(table.minimum_pickup_current.query_index, directional_current_relay.minimum_pickup_current) + insert.add_value(table.current_limit_1.query_index, directional_current_relay.current_limit_1) + insert.add_value(table.inverse_time_flag.query_index, directional_current_relay.inverse_time_flag) + insert.add_value(table.time_delay_1.query_index, directional_current_relay.time_delay_1) + + return self._save_protection_relay_function(table, insert, directional_current_relay, "directional current relay") + def save_distance_relay(self, distance_relay: DistanceRelay) -> bool: """ Save the :class:`DistanceRelay` fields to :class:`TableDistanceRelays`. @@ -843,6 +924,19 @@ def _save_structure(self, table: TableStructures, insert: PreparedStatement, str # IEC61968 Common # ################### + def _save_electronic_address( + self, + table: TableElectronicAddresses, + insert: PreparedStatement, + electronic_address: ElectronicAddress, + description: str + ) -> bool: + insert.add_value(table.email_1.query_index, electronic_address.email1) + insert.add_value(table.is_primary.query_index, electronic_address.is_primary) + insert.add_value(table.description.query_index, electronic_address.description) + + return self._try_execute_single_update(insert, description) + def save_location(self, location: Location) -> bool: """ Save the :class:`Location` fields to :class:`TableLocations`. @@ -913,11 +1007,25 @@ def _insert_street_detail(table: TableStreetAddresses, insert: PreparedStatement insert.add_value(table.suite_number.query_index, street_detail.suite_number if street_detail else None) insert.add_value(table.type.query_index, street_detail.type if street_detail else None) insert.add_value(table.display_address.query_index, street_detail.display_address if street_detail else None) + insert.add_value(table.building_number.query_index, street_detail.building_number if street_detail else None) + + def _save_telephone_number(self, table: TableTelephoneNumbers, insert: PreparedStatement, telephone_number: TelephoneNumber, description: str): + insert.add_value(table.area_code.query_index, telephone_number.area_code) + insert.add_value(table.city_code.query_index, telephone_number.city_code) + insert.add_value(table.country_code.query_index, telephone_number.country_code) + insert.add_value(table.dial_out.query_index, telephone_number.dial_out) + insert.add_value(table.extension.query_index, telephone_number.extension) + insert.add_value(table.local_number.query_index, telephone_number.local_number) + insert.add_value(table.is_primary.query_index, telephone_number.is_primary) + insert.add_value(table.description.query_index, telephone_number.description) + + return self._try_execute_single_update(insert, description) @staticmethod def _insert_town_detail(table: TableTownDetails, insert: PreparedStatement, town_detail: Optional[TownDetail]): insert.add_value(table.town_name.query_index, town_detail.name if town_detail else None) insert.add_value(table.state_or_province.query_index, town_detail.state_or_province if town_detail else None) + insert.add_value(table.country.query_index, town_detail.country if town_detail else None) ##################################### # IEC61968 InfIEC61968 InfAssetInfo # @@ -1045,8 +1153,17 @@ def save_usage_point(self, usage_point: UsagePoint) -> bool: for it in usage_point.equipment: status = status and self._save_equipment_to_usage_point_association(it, usage_point) + for it in usage_point.contacts: + status = status and self._save_usage_point_contact_details(usage_point, it) + return status and self._save_identified_object(table, insert, usage_point, "usage point") + @db_wrapper(TableUsagePointsContactDetails) + def _save_usage_point_contact_details(self, usage_point: UsagePoint, contact_details: ContactDetails, table, insert) -> bool: + insert.add_value(table.usage_point_mrid.query_index, usage_point.mrid) + + return self._save_contact_details(table, insert, contact_details, f"contact details [{contact_details.id}] for usage point {usage_point.mrid}") + ####################### # IEC61968 Operations # ####################### diff --git a/src/zepben/ewb/database/sqlite/network/network_database_tables.py b/src/zepben/ewb/database/sqlite/network/network_database_tables.py index 4e296ae5..56971747 100644 --- a/src/zepben/ewb/database/sqlite/network/network_database_tables.py +++ b/src/zepben/ewb/database/sqlite/network/network_database_tables.py @@ -26,11 +26,15 @@ from zepben.ewb.database.sqlite.tables.associations.table_usage_points_end_devices import * from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_reclose_delays import * from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_relay_info import * +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_electronic_addresses import TableContactDetailsElectronicAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_street_addresses import TableContactDetailsStreetAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_telephone_numbers import TableContactDetailsTelephoneNumbers from zepben.ewb.database.sqlite.tables.extensions.iec61968.metering.table_pan_demand_response_functions import TablePanDemandResponseFunctions from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.core.table_sites import * from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_loops import * from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_lv_feeders import * from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.generation.production.table_ev_charging_units import * +from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_directional_current_relay import TableDirectionalCurrentRelay from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_distance_relays import * from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_protection_relay_function_thresholds import * from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_protection_relay_function_time_limits import * @@ -60,6 +64,7 @@ from zepben.ewb.database.sqlite.tables.iec61968.infiec61968.infassets.table_poles import * from zepben.ewb.database.sqlite.tables.iec61968.metering.table_meters import * from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points import * +from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points_contact_details import TableUsagePointsContactDetails from zepben.ewb.database.sqlite.tables.iec61968.operations.table_operational_restrictions import * from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_current_transformers import * from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_fault_indicators import * @@ -149,12 +154,16 @@ def _included_tables(self) -> Generator[SqliteTable, None, None]: yield TableCircuitsTerminals() yield TableClamps() yield TableConnectivityNodes() + yield TableContactDetailsElectronicAddresses() + yield TableContactDetailsStreetAddresses() + yield TableContactDetailsTelephoneNumbers() yield TableControls() yield TableCurrentRelays() yield TableCurrentTransformerInfo() yield TableCurrentTransformers() yield TableCurveData() yield TableCuts() + yield TableDirectionalCurrentRelay() yield TableDisconnectors() yield TableDiscretes() yield TableDistanceRelays() @@ -238,5 +247,6 @@ def _included_tables(self) -> Generator[SqliteTable, None, None]: yield TableTransformerStarImpedances() yield TableTransformerTankInfo() yield TableUsagePoints() + yield TableUsagePointsContactDetails() yield TableUsagePointsEndDevices() yield TableVoltageRelays() diff --git a/src/zepben/ewb/database/sqlite/network/network_service_reader.py b/src/zepben/ewb/database/sqlite/network/network_service_reader.py index 7cbf4a32..81064dc6 100644 --- a/src/zepben/ewb/database/sqlite/network/network_service_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_service_reader.py @@ -29,11 +29,15 @@ from zepben.ewb.database.sqlite.tables.associations.table_usage_points_end_devices import TableUsagePointsEndDevices from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_reclose_delays import TableRecloseDelays from zepben.ewb.database.sqlite.tables.extensions.iec61968.assetinfo.table_relay_info import TableRelayInfo +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_electronic_addresses import TableContactDetailsElectronicAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_street_addresses import TableContactDetailsStreetAddresses +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details_telephone_numbers import TableContactDetailsTelephoneNumbers from zepben.ewb.database.sqlite.tables.extensions.iec61968.metering.table_pan_demand_response_functions import TablePanDemandResponseFunctions from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.core.table_sites import TableSites from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_loops import TableLoops from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.feeder.table_lv_feeders import TableLvFeeders from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.generation.production.table_ev_charging_units import TableEvChargingUnits +from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_directional_current_relay import TableDirectionalCurrentRelay from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_distance_relays import TableDistanceRelays from zepben.ewb.database.sqlite.tables.extensions.iec61970.base.protection.table_protection_relay_function_thresholds import \ TableProtectionRelayFunctionThresholds @@ -65,6 +69,7 @@ from zepben.ewb.database.sqlite.tables.iec61968.infiec61968.infassets.table_poles import TablePoles from zepben.ewb.database.sqlite.tables.iec61968.metering.table_meters import TableMeters from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points import TableUsagePoints +from zepben.ewb.database.sqlite.tables.iec61968.metering.table_usage_points_contact_details import TableUsagePointsContactDetails from zepben.ewb.database.sqlite.tables.iec61968.operations.table_operational_restrictions import TableOperationalRestrictions from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_current_transformers import TableCurrentTransformers from zepben.ewb.database.sqlite.tables.iec61970.base.auxiliaryequipment.table_fault_indicators import TableFaultIndicators @@ -149,7 +154,7 @@ def __init__( # This is not strictly necessary, it is just to update the type of the reader. It could be done with a generic # on the base class which looks like it works, but that actually silently breaks code insight and completion - self._reader = reader + self._reader: NetworkCimReader = reader def _do_load(self) -> bool: return all([ @@ -176,6 +181,10 @@ def _do_load(self) -> bool: self._load_each(TableMeters, self._reader.load_meter), self._load_each(TableEndDevicesEndDeviceFunctions, self._reader.load_end_devices_end_device_functions), self._load_each(TableUsagePoints, self._reader.load_usage_point), + self._load_each(TableUsagePointsContactDetails, self._reader.load_usage_points_contact_details), + self._load_each(TableContactDetailsElectronicAddresses, self._reader.load_contact_details_electronic_addresses), + self._load_each(TableContactDetailsStreetAddresses, self._reader.load_contact_details_street_addresses), + self._load_each(TableContactDetailsTelephoneNumbers, self._reader.load_contact_details_telephone_number), self._load_each(TableOperationalRestrictions, self._reader.load_operational_restriction), self._load_each(TableBaseVoltages, self._reader.load_base_voltage), self._load_each(TableConnectivityNodes, self._reader.load_connectivity_node), @@ -194,6 +203,7 @@ def _do_load(self) -> bool: self._load_each(TableClamps, self._reader.load_clamp), self._load_each(TableCuts, self._reader.load_cut), self._load_each(TableCurrentRelays, self._reader.load_current_relay), + self._load_each(TableDirectionalCurrentRelay, self._reader.load_directional_current_relay), self._load_each(TableDistanceRelays, self._reader.load_distance_relay), self._load_each(TableVoltageRelays, self._reader.load_voltage_relay), self._load_each(TableProtectionRelayFunctionThresholds, self._reader.load_protection_relay_function_threshold), diff --git a/src/zepben/ewb/database/sqlite/network/network_service_writer.py b/src/zepben/ewb/database/sqlite/network/network_service_writer.py index bda35599..dc627ae3 100644 --- a/src/zepben/ewb/database/sqlite/network/network_service_writer.py +++ b/src/zepben/ewb/database/sqlite/network/network_service_writer.py @@ -14,6 +14,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.generation.production.ev_charging_unit import EvChargingUnit +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.distance_relay import DistanceRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem @@ -116,7 +117,7 @@ def __init__( # This is not strictly necessary, it is just to update the type of the writer. It could be done with a generic # on the base class which looks like it works, but that actually silently breaks code insight and completion - self._writer = writer + self._writer: NetworkCimWriter = writer def _do_save(self) -> bool: return all([ @@ -192,6 +193,7 @@ def _do_save(self) -> bool: self._save_each_object(CurrentRelay, self._writer.save_current_relay), self._save_each_object(TapChangerControl, self._writer.save_tap_changer_control), self._save_each_object(EvChargingUnit, self._writer.save_ev_charging_unit), + self._save_each_object(DirectionalCurrentRelay, self._writer.save_directional_current_relay), self._save_each_object(DistanceRelay, self._writer.save_distance_relay), self._save_each_object(ProtectionRelayScheme, self._writer.save_protection_relay_scheme), self._save_each_object(ProtectionRelaySystem, self._writer.save_protection_relay_system), diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/__init__.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/__init__.py new file mode 100644 index 00000000..e7d95cd5 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details.py new file mode 100644 index 00000000..f93ae6d2 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details.py @@ -0,0 +1,30 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC + +from zepben.ewb.database.sqlite.tables.sqlite_table import SqliteTable +from zepben.ewb.database.sql.column import Type, Column, Nullable + + +class TableContactDetails(SqliteTable, ABC): + """ + A class representing the `ContactDetails` columns required for the database table. + + :var id: A column storing the identifier for this contact, could be autogenerated. + :var contact_type: A column storing the type of contact, e.g. Account Owner. + :var first_name: A column storing the first name of the contact. + :var last_name: A column storing the last name of the contact. + :var preferred_contact_method: A column storing the preferred contact method for this contact. + :var is_primary: A column storing whether this contact is a primary contact. + :var business_name: A column storing the business name of this contact. + """ + def __init__(self): + self.id: Column = self._create_column('id', Type.STRING, Nullable.NOT_NULL) + self.contact_type = self._create_column('contact_type', Type.STRING, Nullable.NULL) + self.first_name = self._create_column('first_name', Type.STRING, Nullable.NULL) + self.last_name = self._create_column('last_name', Type.STRING, Nullable.NULL) + self.preferred_contact_method = self._create_column('preferred_contact_method', Type.STRING, Nullable.NULL) + self.is_primary = self._create_column('is_primary', Type.BOOLEAN, Nullable.NULL) + self.business_name = self._create_column('business_name', Type.STRING, Nullable.NULL) diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_electronic_addresses.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_electronic_addresses.py new file mode 100644 index 00000000..919d561a --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_electronic_addresses.py @@ -0,0 +1,34 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["TableContactDetailsElectronicAddresses"] + +from typing import Generator, List + +from zepben.ewb.database.sql.column import Type, Nullable, Column +from zepben.ewb.database.sqlite.tables.iec61968.common.table_electronic_addresses import TableElectronicAddresses + + +class TableContactDetailsElectronicAddresses(TableElectronicAddresses): + """ + A class representing the ``ContactDetails`` to ``ElectronicAddresses`` association columns required for the database table. + + :var contact_details_id: A column that stores the identifier of the contact details associated with the electronic address. + """ + def __init__(self): + super().__init__() + self.contact_details_id: Column = self._create_column('contact_details_id', Type.STRING, Nullable.NULL) + + @property + def name(self): + return 'contact_details_electronic_addresses' + + @property + def unique_index_columns(self) -> Generator[List[Column], None, None]: + yield [self.contact_details_id, self.email_1] + + @property + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: + yield [self.contact_details_id] \ No newline at end of file diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_street_addresses.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_street_addresses.py new file mode 100644 index 00000000..5beec280 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_street_addresses.py @@ -0,0 +1,29 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import Generator, List + +from zepben.ewb.database.sqlite.tables.iec61968.common.table_street_addresses import TableStreetAddresses +from zepben.ewb.database.sql.column import Type, Column, Nullable + + +class TableContactDetailsStreetAddresses(TableStreetAddresses): + """ + A class representing the `ContactDetails` to `StreetAddress` association columns required for the database table. + + :var contact_details_id: A column that stores the identifier of the contact details associated with the street address. + """ + + def __init__(self): + super().__init__() + self.contact_details_id: Column = self._create_column('contact_details_id', Type.STRING, Nullable.NOT_NULL) + + @property + def name(self) -> str: + return 'contact_details_street_addresses' + + @property + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: + yield from super().non_unique_index_columns + yield [self.contact_details_id] \ No newline at end of file diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_telephone_numbers.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_telephone_numbers.py new file mode 100644 index 00000000..14f4d001 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61968/common/table_contact_details_telephone_numbers.py @@ -0,0 +1,27 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import Generator, List + +from zepben.ewb.database.sql.column import Type, Column, Nullable +from zepben.ewb.database.sqlite.tables.iec61968.common.table_telephone_numbers import TableTelephoneNumbers + + +class TableContactDetailsTelephoneNumbers(TableTelephoneNumbers): + """ + A class representing the `ContactDetails` to `TelephoneNumber` association columns required for the database table. + + :var contact_details_id: A column that stores the identifier of the contact details associated with the street address. + """ + def __init__(self): + super().__init__() + self.contact_details_id: Column = self._create_column('contact_details_id', Type.STRING, Nullable.NOT_NULL) + + @property + def name(self) -> str: + return 'contact_details_telephone_numbers' + + @property + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: + yield [self.contact_details_id] diff --git a/src/zepben/ewb/database/sqlite/tables/extensions/iec61970/base/protection/table_directional_current_relay.py b/src/zepben/ewb/database/sqlite/tables/extensions/iec61970/base/protection/table_directional_current_relay.py new file mode 100644 index 00000000..d125f828 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/extensions/iec61970/base/protection/table_directional_current_relay.py @@ -0,0 +1,35 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from zepben.ewb import TableProtectionRelayFunctions +from zepben.ewb.database.sql.column import Type, Nullable, Column + + +class TableDirectionalCurrentRelay(TableProtectionRelayFunctions): + """ + A class representing the DirectionalCurrentRelay columns required for the database table. + + :var directional_characteristic_angle: A column storing the characteristic angle (in degrees) that defines the boundary between the operate and restrain regions of the directional element, relative to the polarizing quantity. Often referred to as Maximum Torque Angle (MTA) or Relay Characteristic Angle (RCA). + :var polarizing_quantity_type: A column storing the type of voltage to be used for polarization. This guides the selection/derivation of voltage from the VTs. + :var relay_element_phase: A column storing the phase associated with this directional relay element. This helps in selecting the correct 'self-phase' or other phase-derived. + :var minimum_pickup_current: A column storing the minimum current magnitude required for the directional element to operate reliably and determine direction. This might be different from the main pickupCurrent for the overcurrent function. + :var current_limit_1: A column storing the current limit number 1 for inverse time pickup in amperes. + :var inverse_time_flag: A column storing the true if the current relay has inverse time characteristic. + :var time_delay_1: A column storing the inverse time delay number 1 for current limit number 1 in seconds. + + """ + def __init__(self): + super().__init__() + + self.directional_characteristic_angle: Column = self._create_column("directional_characteristic_angle", Type.DOUBLE, Nullable.NULL) + self.polarizing_quantity_type: Column = self._create_column("polarizing_quantity_type", Type.STRING, Nullable.NULL) + self.relay_element_phase: Column = self._create_column("relay_element_phase", Type.STRING, Nullable.NULL) + self.minimum_pickup_current: Column = self._create_column("minimum_pickup_current", Type.DOUBLE, Nullable.NULL) + self.current_limit_1: Column = self._create_column("current_limit_1", Type.DOUBLE, Nullable.NULL) + self.inverse_time_flag: Column = self._create_column("inverse_time_flag", Type.BOOLEAN, Nullable.NULL) + self.time_delay_1: Column = self._create_column("time_delay_1", Type.DOUBLE, Nullable.NULL) + + @property + def name(self) -> str: + return 'directional_current_relays' diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_agreements.py b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_agreements.py index 360ba582..f256d388 100644 --- a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_agreements.py +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_agreements.py @@ -7,8 +7,12 @@ from abc import ABC +from zepben.ewb import Column, Nullable from zepben.ewb.database.sqlite.tables.iec61968.common.table_documents import TableDocuments class TableAgreements(TableDocuments, ABC): - pass + def __init__(self): + super().__init__() + self.validity_interval_start: Column = self._create_column("validity_interval_start", "TEXT", Nullable.NULL) + self.validity_interval_end: Column = self._create_column("validity_interval_end", "TEXT", Nullable.NULL) diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_electronic_addresses.py b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_electronic_addresses.py new file mode 100644 index 00000000..20b86e30 --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_electronic_addresses.py @@ -0,0 +1,26 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["TableElectronicAddresses"] + +from abc import ABC + +from zepben.ewb.database.sqlite.tables.sqlite_table import SqliteTable +from zepben.ewb.database.sql.column import Column, Type, Nullable + + +class TableElectronicAddresses(SqliteTable, ABC): + """ + A class representing the ElectronicAddress columns required for the database table. + + :var email_1: A column storing the primary email address. + :var is_primary: A column storing whether this email is the primary email address of the contact. + :var description: A column storing a description for this email, e.g: work, personal. + """ + def __init__(self): + super().__init__() + self.email_1: Column = self._create_column("email_1", Type.STRING, Nullable.NULL) + self.is_primary: Column = self._create_column("is_primary", Type.STRING) + self.description: Column = self._create_column("description", Type.STRING, Nullable.NULL) \ No newline at end of file diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_street_addresses.py b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_street_addresses.py index 15a1d6be..05533794 100644 --- a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_street_addresses.py +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_street_addresses.py @@ -7,20 +7,35 @@ from abc import ABC -from zepben.ewb.database.sql.column import Column, Nullable +from zepben.ewb.database.sql.column import Column, Nullable, Type from zepben.ewb.database.sqlite.tables.iec61968.common.table_town_details import TableTownDetails class TableStreetAddresses(TableTownDetails, ABC): + """ + A class representing the ElectronicAddress columns required for the database table. + + :var postal_code: A column storing the postal code for the address. + :var po_box: A column storing the post office box. + :var building_name: A column storing the name of a building. + :var floor_identification: A column storing the identification by name or number, expressed as text, of the floor in the building as part of this address. + :var street_name: A column storing the name of the street. + :var number: A column storing the designator of the specific location on the street. + :var suite_number: A column storing the number of the apartment or suite. + :var type: A column storing the type of street. Examples include: street, circle, boulevard, avenue, road, drive, etc. + :var display_address: A column storing the address as it should be displayed to a user. + :var building_number: A column storing the number of the building. + """ def __init__(self): super().__init__() - self.postal_code: Column = self._create_column("postal_code", "TEXT", Nullable.NULL) - self.po_box: Column = self._create_column("po_box", "TEXT", Nullable.NULL) - self.building_name: Column = self._create_column("building_name", "TEXT", Nullable.NULL) - self.floor_identification: Column = self._create_column("floor_identification", "TEXT", Nullable.NULL) - self.street_name: Column = self._create_column("name", "TEXT", Nullable.NULL) - self.number: Column = self._create_column("number", "TEXT", Nullable.NULL) - self.suite_number: Column = self._create_column("suite_number", "TEXT", Nullable.NULL) - self.type: Column = self._create_column("type", "TEXT", Nullable.NULL) - self.display_address: Column = self._create_column("display_address", "TEXT", Nullable.NULL) + self.postal_code: Column = self._create_column("postal_code", Type.STRING , Nullable.NULL) + self.po_box: Column = self._create_column("po_box", Type.STRING, Nullable.NULL) + self.building_name: Column = self._create_column("building_name", Type.STRING, Nullable.NULL) + self.floor_identification: Column = self._create_column("floor_identification", Type.STRING, Nullable.NULL) + self.street_name: Column = self._create_column("name", Type.STRING, Nullable.NULL) + self.number: Column = self._create_column("number", Type.STRING, Nullable.NULL) + self.suite_number: Column = self._create_column("suite_number", Type.STRING, Nullable.NULL) + self.type: Column = self._create_column("type", Type.STRING, Nullable.NULL) + self.display_address: Column = self._create_column("display_address", Type.STRING, Nullable.NULL) + self.building_number: Column = self._create_column("building_number", Type.STRING, Nullable.NULL) diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_telephone_numbers.py b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_telephone_numbers.py new file mode 100644 index 00000000..3a86384c --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_telephone_numbers.py @@ -0,0 +1,35 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC + +from zepben.ewb.database.sqlite.tables.sqlite_table import SqliteTable +from zepben.ewb.database.sql.column import Type, Column, Nullable + + +class TableTelephoneNumbers(SqliteTable, ABC): + """ + A class representing the TelephoneNumber columns required for the database table. + + :var area_code: A column storing the area or region code. + :var city_code: A column storing the city code. + :var country_code: A column storing the country code. + :var dial_out: A column storing the dial out code, for instance to call outside an enterprise. + :var extension: A column storing the extension for this telephone number. + :var international_prefix: A column storing the prefix used when calling an international number. + :var local_number: A column storing the main (local) part of this telephone number. + :var is_primary: A column storing indicating if this phone number is the primary number. + :var description: A column storing the description for phone number, e.g: home, work, mobile. + """ + def __init__(self): + super().__init__() + self.area_code: Column = self._create_column('area_code', Type.STRING, Nullable.NULL) + self.city_code: Column = self._create_column('city_code', Type.STRING, Nullable.NULL) + self.country_code: Column = self._create_column('country_code', Type.STRING, Nullable.NULL) + self.dial_out: Column = self._create_column('dial_out', Type.STRING, Nullable.NULL) + self.extension: Column = self._create_column('extension', Type.STRING, Nullable.NULL) + self.international_prefix: Column = self._create_column('international_prefix', Type.STRING, Nullable.NULL) + self.local_number: Column = self._create_column('local_number', Type.STRING, Nullable.NULL) + self.is_primary: Column = self._create_column('is_primary', Type.BOOLEAN, Nullable.NULL) + self.description: Column = self._create_column('description', Type.STRING, Nullable.NULL) diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_town_details.py b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_town_details.py index af7a87c0..23b31067 100644 --- a/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_town_details.py +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/common/table_town_details.py @@ -7,13 +7,21 @@ from abc import ABC -from zepben.ewb.database.sql.column import Column, Nullable +from zepben.ewb.database.sql.column import Column, Nullable, Type from zepben.ewb.database.sqlite.tables.sqlite_table import SqliteTable class TableTownDetails(SqliteTable, ABC): + """ + A class representing the TownDetail columns required for the database table. + + :var town_name: A column storing the town name. + :var state_or_province: A column storing the name of the state or province. + :var country: A column storing the name of the country. + """ def __init__(self): super().__init__() - self.town_name: Column = self._create_column("town_name", "TEXT", Nullable.NULL) - self.state_or_province: Column = self._create_column("state_or_province", "TEXT", Nullable.NULL) + self.town_name: Column = self._create_column("town_name", Type.STRING, Nullable.NULL) + self.state_or_province: Column = self._create_column("state_or_province", Type.STRING, Nullable.NULL) + self.country: Column = self._create_column("country", Type.STRING, Nullable.NULL) diff --git a/src/zepben/ewb/database/sqlite/tables/iec61968/metering/table_usage_points_contact_details.py b/src/zepben/ewb/database/sqlite/tables/iec61968/metering/table_usage_points_contact_details.py new file mode 100644 index 00000000..e839abde --- /dev/null +++ b/src/zepben/ewb/database/sqlite/tables/iec61968/metering/table_usage_points_contact_details.py @@ -0,0 +1,27 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import Generator, List + +from zepben.ewb.database.sql.column import Type, Nullable, Column +from zepben.ewb.database.sqlite.tables.extensions.iec61968.common.table_contact_details import TableContactDetails + + +class TableUsagePointsContactDetails(TableContactDetails): + """ + A class representing the UsagePoint to ContactDetails association columns required for the database table. + + :var usage_point_mrid: A column that stores the identifier of the usage point associated with the contact details. + """ + def __init__(self): + super().__init__() + self.usage_point_mrid: Column = self._create_column('usage_point_mrid', Type.STRING, Nullable.NULL) + + @property + def name(self) -> str: + return "usage_point_contact_details" + + @property + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: + yield [self.usage_point_mrid] diff --git a/src/zepben/ewb/database/sqlite/tables/table_version.py b/src/zepben/ewb/database/sqlite/tables/table_version.py index 74396012..6af09a2f 100644 --- a/src/zepben/ewb/database/sqlite/tables/table_version.py +++ b/src/zepben/ewb/database/sqlite/tables/table_version.py @@ -14,7 +14,7 @@ class TableVersion(SqliteTable): - SUPPORTED_VERSION = 59 + SUPPORTED_VERSION = 63 def __init__(self): super().__init__() diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/__init__.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/__init__.py new file mode 100644 index 00000000..36bdf511 --- /dev/null +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py new file mode 100644 index 00000000..4d17a86f --- /dev/null +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -0,0 +1,164 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["ContactDetails"] + +from typing import Generator, Any + +from zepben.ewb.util import CopyableUUID, ngen, nlen +from zepben.ewb.dataclassy import dataclass +from zepben.ewb.model.cim.extensions.zbex import zbex +from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress +from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress + +from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType +from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber + + +@zbex +@dataclass +class ContactDetails: + """[ZBEX] The details required to contact a person or company.""" + + id: str = CopyableUUID() + """[ZBEX] The identifier for this contact, could be autogenerated.""" + + contact_address: StreetAddress | None = None + """[ZBEX] Contact address, potentially different than 'streetAddress' (e.g., another city).""" + + contact_type: str | None = None + """[ZBEX] The type of contact, e.g. Account Owner.""" + + first_name: str | None = None + """[ZBEX] First name of the contact.""" + + last_name: str | None = None + """[ZBEX] Last name of the contact.""" + + preferred_contact_method: ContactMethodType = ContactMethodType.UNKNOWN + """[ZBEX] The preferred contact method for this contact.""" + + is_primary: bool | None = None + """[ZBEX] Whether this contact is a primary contact.""" + + business_name: str | None = None + """[ZBEX] The business name of this contact.""" + + _phone_numbers: list[TelephoneNumber] | None = None + + _electronic_addresses: list[ElectronicAddress] | None = None + + def __init__(self, phone_numbers: list[TelephoneNumber] = None, electronic_addresses: list[ElectronicAddress] = None): + for number in phone_numbers or []: + self.add_phone_number(number) + + for email in electronic_addresses or []: + self.add_electronic_address(email) + + + @property + def phone_numbers(self) -> Generator[TelephoneNumber, None, None]: + return ngen(self._phone_numbers) + + @property + def num_phone_numbers(self) -> int: + """Get the number of entries in the ``TelephoneNumber`` collection.""" + return nlen(self._phone_numbers) + + def add_phone_number(self, phone_number: TelephoneNumber) -> "ContactDetails": + """ + Add an ``TelephoneNumber`` to this ``ContactDetails``. + + :param phone_number: The ``TelephoneNumber`` to add. + :return: This ``ContactDetails`` for fluent use. + """ + if self._phone_numbers is None: + self._phone_numbers = [] + self._phone_numbers.append(phone_number) + return self + + def remove_phone_number(self, phone_number: TelephoneNumber) -> bool: + """ + Remove an ``TelephoneNumber`` from this ``ContactDetails``. + :param phone_number: The ``TelephoneNumber`` to remove. + :return: True if the ``TelephoneNumber`` was removed. + """ + try: + self._phone_numbers.remove(phone_number) + if self.num_phone_numbers == 0: + self._phone_numbers = None + return True + except ValueError: + return False + + def clear_phone_numbers(self) -> "ContactDetails": + """ + Clear all ``TelephoneNumber``'s from this ``ContactDetails``. + :return: this ``ContactDetails`` for fluent use. + """ + self._phone_numbers = None + return self + + @property + def electronic_addresses(self) -> Generator[ElectronicAddress, None, None]: + return ngen(self._electronic_addresses) + + def num_electronic_addresses(self) -> int: + """Get the number of entries in the [ElectronicAddress] collection.""" + return nlen(self._electronic_addresses) + + def add_electronic_address(self, electronic_address: ElectronicAddress) -> "ContactDetails": + """ + Add an ``ElectronicAddress`` to this ``ContactDetails``. + :param electronic_address: The ``ElectronicAddress`` to add. + :return: this ``ContactDetails`` for fluent use. + """ + if self._electronic_addresses is None: + self._electronic_addresses = [] + self._electronic_addresses.append(electronic_address) + return self + + def remove_electronic_address(self, electronic_address: ElectronicAddress) -> bool: + """ + Remove an ``ElectronicAddress`` from this ``ContactDetails``. + :param electronic_address: The ``ElectronicAddress`` to remove. + :return: True if the ``ElectronicAddress`` was removed. + """ + try: + self._electronic_addresses.remove(electronic_address) + if self.num_electronic_addresses == 0: + self._electronic_addresses = None + return True + except ValueError: + return False + + def clear_electronic_addresses(self) -> "ContactDetails": + """ + Clear all ``ElectronicAddress``'s from this ``ContactDetails``. + + :return: this ``ContactDetails`` for fluent use. + """ + self._electronic_addresses = None + return self + + def __eq__(self, other: Any) -> bool: + # + # NOTE: We implement the equals to allow us to compare the entire ``ContactDetails`` as a value. We do this since it isnt an + # ``IdentifiedObject``, so our other helpers don't support it. + if not isinstance(other, ContactDetails): + return False + return not any(( + self.is_primary != other.is_primary, + self.id != other.id, + self.contact_address != other.contact_address, + self.contact_type != other.contact_type, + self.first_name != other.first_name, + self.last_name != other.last_name, + self.preferred_contact_method != other.preferred_contact_method, + self.business_name != other.business_name, + self._phone_numbers != other._phone_numbers, + self._electronic_addresses != other._electronic_addresses, + )) + \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_method_type.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_method_type.py new file mode 100644 index 00000000..3c9e5dc6 --- /dev/null +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_method_type.py @@ -0,0 +1,22 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["ContactMethodType"] + +from enum import Enum + +from zepben.ewb.model.cim.extensions.zbex import zbex + + +@zbex +class ContactMethodType(Enum): + UNKNOWN = 0 + EMAIL = 1 + CALL = 2 + LETTER = 3 + + @property + def short_name(self): + return str(self)[18:] diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py new file mode 100644 index 00000000..2f781807 --- /dev/null +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py @@ -0,0 +1,41 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ['DirectionalCurrentRelay'] + +from zepben.ewb import ProtectionRelayFunction, PhaseCode +from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType +from zepben.ewb.model.cim.extensions.zbex import zbex + + +@zbex +class DirectionalCurrentRelay(ProtectionRelayFunction): + """ + [ZBEX] A Directional Current Relay is a type of protective relay used in electrical power systems to detect the direction of current flow and operate only + when the current exceeds a certain threshold in a specified direction. + """ + #zbex + directional_characteristic_angle: float | None = None + """[ZBEX] The characteristic angle (in degrees) that defines the boundary between the operate and restrain regions of the directional element, relative + to the polarizing quantity. Often referred to as Maximum Torque Angle (MTA) or Relay Characteristic Angle (RCA)""" + + polarizing_quantity_type: PolarizingQuantityType | None = None + """[ZBEX] Specifies the type of voltage to be used for polarization. This guides the selection/derivation of voltage from the VTs.""" + + relay_element_phase: PhaseCode | None = None + """[ZBEX] The phase associated with this directional relay element. This helps in selecting the correct 'self-phase' or other phase-derived.""" + + minimum_pickup_current: float | None = None + """[ZBEX] The minimum current magnitude required for the directional element to operate reliably and determine direction. This might be different from + the main pickupCurrent for the overcurrent function.""" + + current_limit_1: float | None = None + """[ZBEX]Current limit number 1 for inverse time pickup in amperes.""" + + inverse_time_flag: bool | None = None + """[ZBEX] Set True if the current relay has inverse time characteristics.""" + + time_delay_1: float | None = None + """[ZBEX] Inverse time delay number 1 for current limit number 1 in seconds.""" diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/polarizing_quantity_type.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/polarizing_quantity_type.py new file mode 100644 index 00000000..9641ee2e --- /dev/null +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/polarizing_quantity_type.py @@ -0,0 +1,40 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["PolarizingQuantityType"] + +from enum import Enum + +from zepben.ewb.model.cim.extensions.zbex import zbex + + +@zbex +class PolarizingQuantityType(Enum): + """ + [ZBEX] Defines the type of polarizing quantity used by the directional relay. This informs how the relay + determines the reference voltage from the Voltage transformers associated with its parent ProtectionEquipment. + """ + + UNKNOWN = 0 + """[ZBEX] Type is unknown.""" + + SELF_PHASE_VOLTAGE = 1 + """[ZBEX] Uses the voltage of the same phase as the current element (e.g., Va for an Ia element).""" + + QUADRATURE_VOLTAGE = 2 + """[ZBEX] Uses a quadrature voltage (e.g., Vbc for an Ia element, specific convention applies.""" + + ZERO_SEQUENCE_VOLTAGE = 3 + """[ZBEX] Uses the zero sequence voltage (Vo), derived from three phase voltages.""" + + NEGATIVE_SEQUENCE_VOLTAGE = 4 + """[ZBEX] Uses the negative sequence voltage (V2), derived from three phase voltages.""" + + POSITIVE_SEQUENCE_VOLTAGE = 5 + """[ZBEX] Uses the positive sequence voltage (V1), derived from three phase voltages.""" + + @property + def short_name(self): + return str(self)[23:] diff --git a/src/zepben/ewb/model/cim/iec61968/common/agreement.py b/src/zepben/ewb/model/cim/iec61968/common/agreement.py index 1559c46e..deabc453 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/common/agreement.py @@ -6,11 +6,16 @@ __all__ = ["Agreement"] from zepben.ewb.model.cim.iec61968.common.document import Document +from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval class Agreement(Document): """ Formal agreement between two parties defining the terms and conditions for a set of services. The specifics of the services are, in turn, defined via one or more service agreements. + + :var validity_interval: Date and time interval this agreement is valid (from going into effect to termination). """ - pass + + validity_interval: DateTimeInterval | None = None + """Date and time interval this agreement is valid (from going into effect to termination).""" diff --git a/src/zepben/ewb/model/cim/iec61968/common/electronic_address.py b/src/zepben/ewb/model/cim/iec61968/common/electronic_address.py new file mode 100644 index 00000000..25270ccd --- /dev/null +++ b/src/zepben/ewb/model/cim/iec61968/common/electronic_address.py @@ -0,0 +1,22 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["ElectronicAddress"] + +from dataclasses import dataclass + + +@dataclass +class ElectronicAddress: + """ + Electronic address information. + """ + + email1: str | None = None + """Primary email address.""" + is_primary: bool | None = None + """[ZBEX] Whether this email is the primary address of the contact.""" + description: str | None = None + """[ZBEX] A description for this email, e.g: work, personal.""" diff --git a/src/zepben/ewb/model/cim/iec61968/common/street_address.py b/src/zepben/ewb/model/cim/iec61968/common/street_address.py index 9621f937..dad927ec 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/street_address.py +++ b/src/zepben/ewb/model/cim/iec61968/common/street_address.py @@ -13,16 +13,23 @@ @dataclass -class StreetAddress(object): +class StreetAddress: """ General purpose street and postal address information. + + :var postal_code: Postal code for the address. + :var town_detail: Optional :class:`TownDetail` for this address. + :var po_box: Post office box for the address. + :var street_detail: Optional :class:`StreetDetail` for this address. """ postal_code: Optional[str] = None - """Postal code for the address.""" town_detail: Optional[TownDetail] = None - """Optional `TownDetail` for this address.""" po_box: Optional[str] = None - """Post office box for the address.""" street_detail: Optional[StreetDetail] = None - """Optional `StreetDetail` for this address.""" + + def __init__(self, postal_code=None, town_detail=None, po_box=None, street_detail=None): + self.postal_code = str(postal_code) if postal_code is not None else None + self.town_detail = town_detail + self.po_box = str(po_box) if po_box is not None else None + self.street_detail = street_detail diff --git a/src/zepben/ewb/model/cim/iec61968/common/street_detail.py b/src/zepben/ewb/model/cim/iec61968/common/street_detail.py index aa2b4aa8..b330a504 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/street_detail.py +++ b/src/zepben/ewb/model/cim/iec61968/common/street_detail.py @@ -6,7 +6,6 @@ __all__ = ["StreetDetail"] from dataclasses import dataclass -from typing import Optional @dataclass @@ -15,32 +14,52 @@ class StreetDetail(object): Street details, in the context of address. """ - building_name: Optional[str] = None + building_name: str | None = None """ (if applicable) In certain cases the physical location of the place of interest does not have a direct point of entry from the street, but may be located inside a larger structure such as a building, complex, office block, apartment, etc. """ - floor_identification: Optional[str] = None + floor_identification: str | None = None """The identification by name or number, expressed as text, of the floor in the building as part of this address.""" - name: Optional[str] = None + name: str | None = None """Name of the street.""" - number: Optional[str] = None + number: str | None = None """Designator of the specific location on the street.""" - suite_number: Optional[str] = None + suite_number: str | None = None """Number of the apartment or suite.""" - type: Optional[str] = None + type: str | None = None """Type of street. Examples include: street, circle, boulevard, avenue, road, drive, etc.""" - display_address: Optional[str] = None + display_address: str | None = None """The address as it should be displayed to a user.""" + building_number: str | None = None + """[ZBEX] The number of the building.""" def all_fields_empty(self): """Check to see if all fields of this `StreetDetail` are empty.""" - return not ( - self.building_name or - self.floor_identification or - self.name or - self.number or - self.suite_number or - self.type or - self.display_address + return not any( + ( + self.building_name, + self.floor_identification, + self.name, + self.number, + self.suite_number, + self.type, + self.display_address, + self.building_number, + ) + ) + + def all_fields_null(self): + """Check to see if all fields of this `StreetDetail` are null.""" + return all( + ( + self.building_name is None, + self.floor_identification is None, + self.name is None, + self.number is None, + self.suite_number is None, + self.type is None, + self.display_address is None, + self.building_number is None, + ) ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/telephone_number.py b/src/zepben/ewb/model/cim/iec61968/common/telephone_number.py new file mode 100644 index 00000000..03d30bbf --- /dev/null +++ b/src/zepben/ewb/model/cim/iec61968/common/telephone_number.py @@ -0,0 +1,55 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +__all__ = ["TelephoneNumber"] + +from dataclasses import field, dataclass + + +@dataclass +class TelephoneNumber: + """ + Telephone number. + """ + + area_code: str | None = None + """(if applicable) Area or region code.""" + city_code: str | None = None + """(if applicable) City code.""" + country_code: str | None = None + """Country code.""" + dial_out: str | None = None + """(if applicable) Dial out code, for instance to call outside an enterprise.""" + extension: str | None = None + """(if applicable) Extension for this telephone number.""" + international_prefix: str | None = None + """(if applicable) Prefix used when calling an international number.""" + itu_phone: str | None = field(default=None) + """Phone number according to ITU E.164. Will return `null` if a valid""" # TODO: lulwut? also jvmsdk + partial_itu_phone: str | None = field(default=None) + """As much of the phone number according to ITU E.164 that could be formatted based on the given fields.""" + local_number: str | None = None + """Main (local) part of this telephone number.""" + is_primary: str | None = None + """[ZBEX] Is this telephone number the primary number.""" + description: str | None = None + """[ZBEX] Description for this phone number, e.g: home, work, mobile.""" + + def __post_init__(self): + if self.country_code is not None and len(it := self.maybe_itu_formatted_phone()) <= 15: + self.itu_phone = it + self.partial_itu_phone = self.maybe_itu_formatted_phone() if self.itu_phone is None else None + + def maybe_itu_formatted_phone(self) -> str | None: + return f"{self.country_code or ''}{self.area_code or ''}{self.city_code or ''}{self.local_number or ''}" or None + + def __str__(self): + def inner(): + yield self.dial_out + yield f"{self.international_prefix or ''}{self.maybe_itu_formatted_phone()}" + if self.extension is not None: + yield f'ext {self.extension}' + + return f"{self.description}: {' '.join(inner())} [primary: {self.is_primary}]" diff --git a/src/zepben/ewb/model/cim/iec61968/common/town_detail.py b/src/zepben/ewb/model/cim/iec61968/common/town_detail.py index bf2fdd2c..db183ced 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/town_detail.py +++ b/src/zepben/ewb/model/cim/iec61968/common/town_detail.py @@ -6,7 +6,6 @@ __all__ = ["TownDetail"] from dataclasses import dataclass -from typing import Optional @dataclass @@ -15,11 +14,21 @@ class TownDetail(object): Town details, in the context of address. """ - name: Optional[str] = None + name: str | None = None """Town name.""" - state_or_province: Optional[str] = None + state_or_province: str | None = None """Name of the state or province.""" + country: str | None = None + """Name of the country""" def all_fields_null_or_empty(self): """Check to see if all fields of this `TownDetail` are null or empty.""" - return not (self.name or self.state_or_province) + return not (self.name or self.state_or_province or self.country) + + def all_fields_null(self): + """Check to see if all fields of this `TownDetail` are null""" + return all(( + self.name is None, + self.state_or_province is None, + self.country is None + )) diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 50976dfc..21abc15f 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -9,6 +9,7 @@ from typing import Optional, List, Generator, TYPE_CHECKING +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove @@ -53,10 +54,11 @@ class UsagePoint(IdentifiedObject): four-wire, s12n (splitSecondary12N) is single-phase, three-wire, and s1n and s2n are single-phase, two-wire. """ - _equipment: Optional[List[Equipment]] = None - _end_devices: Optional[List[EndDevice]] = None + _equipment: list[Equipment] | None = None + _end_devices: list[EndDevice] | None = None + _contacts: list[ContactDetails] | None = None - def __init__(self, equipment: List[Equipment] = None, end_devices: List[EndDevice] = None, **kwargs): + def __init__(self, equipment: List[Equipment] = None, end_devices: List[EndDevice] = None, contacts: List[ContactDetails] = None, **kwargs): super(UsagePoint, self).__init__(**kwargs) if equipment: for eq in equipment: @@ -64,6 +66,9 @@ def __init__(self, equipment: List[Equipment] = None, end_devices: List[EndDevic if end_devices: for ed in end_devices: self.add_end_device(ed) + if contacts: + for c in contacts: + self.add_contact(c) def num_equipment(self): """ @@ -184,3 +189,32 @@ def is_metered(self): Returns True if this `UsagePoint` has an `EndDevice`, False otherwise. """ return nlen(self._end_devices) > 0 + + @property + def contacts(self) -> Generator[ContactDetails, None, None]: + """[ZBEX] All contact details for this `UsagePoint`""" + return ngen(self._contacts) + + def num_contacts(self): + """Get the number of entries in the `ContactDetails` collection""" + return nlen(self._contacts) + + def get_contact(self, id: str) -> ContactDetails | None: + """All End devices at this usage point.""" # TODO: again, lol, also jvmsdk + return get_by_mrid(self._contacts, id) + + def add_contact(self, contact: ContactDetails) -> UsagePoint: + """Add a `ContactDetails` to this `UsagePoint`""" + if self._validate_reference( + other=contact, + get_identifier=lambda it: it.id, + getter=self.get_contact, + type_description=lambda: f"A ContactDetails with ID {contact.id}", + ): + return self + + if self._contacts is None: + self._contacts = list() + self._contacts.append(contact) + return self + diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py index 4010d8c6..1f8154d0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py @@ -18,6 +18,10 @@ logger = logging.getLogger(__name__) +TIdentifiedObject = TypeVar("TIdentifiedObject", bound="IdentifiedObject") +""" +Generic type of IdentifiedObject which can be used for type hinting generics. +""" @dataclass(slots=True) class IdentifiedObject(object, metaclass=ABCMeta): @@ -196,19 +200,35 @@ def clear_names(self) -> IdentifiedObject: return self - def _validate_reference(self, other: IdentifiedObject, getter: Callable[[str], IdentifiedObject], type_descr: str) -> bool: + + @overload + def _validate_reference(self, other: IdentifiedObject, getter: Callable[[str], IdentifiedObject | None], type_description: str) -> bool: ... + + @overload + def _validate_reference(self, other: T, get_identifier: Callable[[Callable], str], getter: Callable[[str], T | None], type_description: Callable[[], str]) -> bool: ... + + # FIXME: in python 3.11, the IdentifiedObject type hint can be replaced with Self, and this can all be moved into the class def. + # @singledispatchmethod + def _validate_reference(self, other: IdentifiedObject | T, getter: Callable[[str], IdentifiedObject | T], type_description: Callable[[], str], get_identifier: Callable[[...], str]=None) -> bool: """ Validate whether a given reference exists to `other` using the provided getter function. :param other: The object to look up with the getter using its mRID. :param getter: A function that takes an mRID and returns an `IdentifiedObject`, and throws a `KeyError` if it couldn't be found. - :param type_descr: The type description to use for the lazily generated error message. Should be of the form "A[n] type(other)" + :param describe_other: The type description to use for the lazily generated error message. Should be of the form "A[n] type(other)" :return: True if `other` was retrieved with `getter` and was equivalent, False otherwise. :raises ValueError: If the object retrieved from `getter` is not `other`. """ + if isinstance(other, IdentifiedObject): + get_identifier = lambda _other: _other.mrid + describe_other = f"{type_description} with mRID {other.mrid}" + else: + require(get_identifier is not None, lambda: "foo") + describe_other = type_description + try: - get_result = getter(other.mrid) - require(get_result is other, lambda: f"{type_descr} with mRID {other.mrid} already exists in {str(self)}") + get_result = getter(get_identifier(other)) + require(get_result is other, lambda: f"{describe_other} already exists in {str(self)}") return True except (KeyError, AttributeError): return False @@ -233,7 +253,5 @@ def _validate_reference_by_field(self, other: IdentifiedObject, field: Any, gett return False -TIdentifiedObject = TypeVar("TIdentifiedObject", bound=IdentifiedObject) -""" -Generic type of IdentifiedObject which can be used for type hinting generics. -""" +T = TypeVar('T', ) + diff --git a/src/zepben/ewb/model/cim/iec61970/base/domain/date_time_interval.py b/src/zepben/ewb/model/cim/iec61970/base/domain/date_time_interval.py new file mode 100644 index 00000000..18ee780a --- /dev/null +++ b/src/zepben/ewb/model/cim/iec61970/base/domain/date_time_interval.py @@ -0,0 +1,27 @@ +# Copyright 2025 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/.4 + +__all__ = ["DateTimeInterval"] + +from dataclasses import dataclass +from datetime import datetime + +from zepben.ewb import require + + +@dataclass +class DateTimeInterval: + """ + Interval between two date and time points, where the interval includes the start time but excludes end time. + """ + start: datetime | None = None + """Start date and time of this interval. The start date and time is included in the defined interval.""" + end: datetime | None = None + """End date and time of this interval. The end date and time where the interval is defined up to, but excluded.""" + + def __post_init__(self): + require(any((self.start, self.end)), lambda: 'You must provide a start or end time.') + if all((self.start, self.end)): + require(self.start <= self.end, lambda: 'The start time must be before the end time.') diff --git a/src/zepben/ewb/services/common/enum_mapper.py b/src/zepben/ewb/services/common/enum_mapper.py index d92b5170..3cadc314 100644 --- a/src/zepben/ewb/services/common/enum_mapper.py +++ b/src/zepben/ewb/services/common/enum_mapper.py @@ -30,6 +30,8 @@ def __init__(self, cim_enum: Type[TCimEnum], pb_enum: EnumTypeWrapper): cim_by_key = {self._extract_key_from_cim(it): it for it in cim_enum} pb_by_key = {self._extract_key_from_pb(it, pb_common_key): it for it in pb_enum.DESCRIPTOR.values} + print(cim_by_key) + print(pb_by_key) self._cim_to_proto = {cim: pb_by_key[key] for key, cim in cim_by_key.items()} diff --git a/src/zepben/ewb/services/common/translator/base_cim2proto.py b/src/zepben/ewb/services/common/translator/base_cim2proto.py index b8ce9a0b..bca7589f 100644 --- a/src/zepben/ewb/services/common/translator/base_cim2proto.py +++ b/src/zepben/ewb/services/common/translator/base_cim2proto.py @@ -57,8 +57,8 @@ def document_to_pb(cim: Document) -> PBDocument: return PBDocument( io=identified_object_to_pb(cim), - createdDateTime=timestamp, **set_or_null( + createdDateTime=timestamp, title=cim.title, authorName=cim.author_name, type=cim.type, diff --git a/src/zepben/ewb/services/common/translator/base_proto2cim.py b/src/zepben/ewb/services/common/translator/base_proto2cim.py index 1dfbf633..b9a54ac7 100644 --- a/src/zepben/ewb/services/common/translator/base_proto2cim.py +++ b/src/zepben/ewb/services/common/translator/base_proto2cim.py @@ -67,7 +67,7 @@ def get_nullable(pb: Message, field: str) -> Optional[T]: @bind_to_cim def document_to_cim(pb: PBDocument, cim: Document, service: BaseService): cim.title = get_nullable(pb, 'title') - cim.created_date_time = pb.createdDateTime.ToDatetime() if pb.HasField("createdDateTime") else None + cim.created_date_time = cdt.ToDatetime() if (cdt := get_nullable(pb, "createdDateTime")) else None cim.author_name = get_nullable(pb, 'authorName') cim.type = get_nullable(pb, 'type') cim.status = get_nullable(pb, 'status') diff --git a/src/zepben/ewb/services/customer/customer_service_comparator.py b/src/zepben/ewb/services/customer/customer_service_comparator.py index 5715f15a..4989f75c 100644 --- a/src/zepben/ewb/services/customer/customer_service_comparator.py +++ b/src/zepben/ewb/services/customer/customer_service_comparator.py @@ -2,7 +2,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from zepben.ewb import BaseServiceComparator, ObjectDifference, Customer, CustomerAgreement, PricingStructure, Tariff +from zepben.ewb import BaseServiceComparator, ObjectDifference, Customer, CustomerAgreement, PricingStructure, Tariff, Agreement # @@ -19,7 +19,8 @@ class CustomerServiceComparator(BaseServiceComparator): ################### def _compare_agreement(self, diff: ObjectDifference) -> ObjectDifference: - return self._compare_document(diff) + self._compare_document(diff) + return self._compare_values(diff, Agreement.validity_interval) ###################### # IEC61968 Customers # diff --git a/src/zepben/ewb/services/customer/translator/customer_cim2proto.py b/src/zepben/ewb/services/customer/translator/customer_cim2proto.py index 1b8e53bf..7c64ee2c 100644 --- a/src/zepben/ewb/services/customer/translator/customer_cim2proto.py +++ b/src/zepben/ewb/services/customer/translator/customer_cim2proto.py @@ -10,12 +10,15 @@ from zepben.protobuf.cim.iec61968.customers.Customer_pb2 import Customer as PBCustomer from zepben.protobuf.cim.iec61968.customers.PricingStructure_pb2 import PricingStructure as PBPricingStructure from zepben.protobuf.cim.iec61968.customers.Tariff_pb2 import Tariff as PBTariff +from zepben.protobuf.cim.iec61970.base.domain.DateTimeInterval_pb2 import DateTimeInterval as PBDateTimeInterval from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.model.cim.iec61968.customers.customer import Customer from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement from zepben.ewb.model.cim.iec61968.customers.pricing_structure import PricingStructure from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff +from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval + from zepben.ewb.services.common.translator.base_cim2proto import document_to_pb, organisation_role_to_pb, set_or_null, bind_to_pb from zepben.ewb.services.common.translator.util import mrid_or_empty # noinspection PyProtectedMember @@ -28,7 +31,18 @@ @bind_to_pb def agreement_to_pb(cim: Agreement) -> PBAgreement: - return PBAgreement(doc=document_to_pb(cim)) + """ + Convert the :class:`Agreement` into its protobuf counterpart. + + :param cim: the :class:`Agreement` to convert. + :return: the new protobuf object for fluent use. + """ + return PBAgreement( + **set_or_null( + validityInterval=date_time_interval_to_pb(cim.validity_interval) + ), + doc=document_to_pb(cim) + ) ###################### @@ -69,3 +83,20 @@ def pricing_structure_to_pb(cim: PricingStructure) -> PBPricingStructure: @bind_to_pb def tariff_to_pb(cim: Tariff) -> PBTariff: return PBTariff(doc=document_to_pb(cim)) + +######################## +# IEC61970 Base Domain # +######################## + +@bind_to_pb +def date_time_interval_to_pb(cim: DateTimeInterval) -> PBDateTimeInterval | None: + """ + Convert the :class:`DateTimeInterval` into its protobuf counterpart. + + :param cim: The :class:`DateTimeInterval` to convert. + :return: the new protobuf object for fluent use. + """ + if cim: + return DateTimeInterval( + **set_or_null(start=cim.start, end=cim.end), + ) \ No newline at end of file diff --git a/src/zepben/ewb/services/customer/translator/customer_proto2cim.py b/src/zepben/ewb/services/customer/translator/customer_proto2cim.py index 7f818ce5..4ca96b6a 100644 --- a/src/zepben/ewb/services/customer/translator/customer_proto2cim.py +++ b/src/zepben/ewb/services/customer/translator/customer_proto2cim.py @@ -14,14 +14,17 @@ from zepben.protobuf.cim.iec61968.customers.Customer_pb2 import Customer as PBCustomer from zepben.protobuf.cim.iec61968.customers.PricingStructure_pb2 import PricingStructure as PBPricingStructure from zepben.protobuf.cim.iec61968.customers.Tariff_pb2 import Tariff as PBTariff +from zepben.protobuf.cim.iec61970.base.domain.DateTimeInterval_pb2 import DateTimeInterval as PBDateTimeInterval import zepben.ewb.services.common.resolver as resolver -from zepben.ewb import organisation_role_to_cim, document_to_cim, BaseService, CustomerKind, bind_to_cim +from zepben.ewb import organisation_role_to_cim, document_to_cim, CustomerKind, bind_to_cim from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.model.cim.iec61968.customers.customer import Customer from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement from zepben.ewb.model.cim.iec61968.customers.pricing_structure import PricingStructure from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff +from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval + from zepben.ewb.services.common.translator.base_proto2cim import get_nullable from zepben.ewb.services.customer.customers import CustomerService @@ -31,8 +34,13 @@ ################### @bind_to_cim -def agreement_to_cim(pb: PBAgreement, cim: Agreement, service: BaseService): - document_to_cim(pb.doc, cim, service) +def agreement_to_cim(pb: PBAgreement, cim: Agreement, service: CustomerService): + if vi := get_nullable(pb, 'validityInterval'): + cim.validity_interval=(date_time_interval_to_cim(vi, service)) + + document_to_cim( + pb.doc, cim, service + ) ###################### @@ -85,3 +93,15 @@ def tariff_to_cim(pb: PBTariff, service: CustomerService) -> Optional[Tariff]: document_to_cim(pb.doc, cim, service) return cim if service.add(cim) else None + + +######################## +# IEC61970 Base Domain # +######################## + +@bind_to_cim +def date_time_interval_to_cim(pb: PBDateTimeInterval, service: CustomerService) -> Optional[DateTimeInterval]: + return DateTimeInterval( + start=get_nullable(pb, 'start'), + end=get_nullable(pb, 'end'), + ) \ No newline at end of file diff --git a/src/zepben/ewb/services/measurement/translator/measurement_cim2proto.py b/src/zepben/ewb/services/measurement/translator/measurement_cim2proto.py index fe1d0011..bc39ed97 100644 --- a/src/zepben/ewb/services/measurement/translator/measurement_cim2proto.py +++ b/src/zepben/ewb/services/measurement/translator/measurement_cim2proto.py @@ -16,6 +16,7 @@ from zepben.ewb.model.cim.iec61970.base.meas.analog_value import AnalogValue from zepben.ewb.model.cim.iec61970.base.meas.discrete_value import DiscreteValue from zepben.ewb.model.cim.iec61970.base.meas.measurement_value import MeasurementValue +from zepben.ewb.services.common.translator.base_cim2proto import set_or_null def analog_value_to_pb(cim: AnalogValue) -> PBAnalogValue: @@ -33,7 +34,7 @@ def discrete_value_to_pb(cim: DiscreteValue) -> PBDiscreteValue: def measurement_value_to_pb(cim: MeasurementValue) -> PBMeasurementValue: ts = Timestamp() ts.FromDatetime(cim.time_stamp) - return PBMeasurementValue(timeStamp=ts) + return PBMeasurementValue(**set_or_null(timeStamp=ts)) AnalogValue.to_pb = analog_value_to_pb diff --git a/src/zepben/ewb/services/measurement/translator/measurement_proto2cim.py b/src/zepben/ewb/services/measurement/translator/measurement_proto2cim.py index f3461cb8..5d4be959 100644 --- a/src/zepben/ewb/services/measurement/translator/measurement_proto2cim.py +++ b/src/zepben/ewb/services/measurement/translator/measurement_proto2cim.py @@ -14,6 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.meas.analog_value import AnalogValue from zepben.ewb.model.cim.iec61970.base.meas.discrete_value import DiscreteValue from zepben.ewb.model.cim.iec61970.base.meas.measurement_value import MeasurementValue +from zepben.ewb.services.common.translator.base_proto2cim import get_nullable from zepben.ewb.services.measurement.measurements import MeasurementService @@ -43,7 +44,7 @@ def discrete_value_to_cim(pb: PBDiscreteValue, service: MeasurementService): def measurement_value_to_cim(pb: PBMeasurementValue, cim: MeasurementValue): - cim.time_stamp = pb.timeStamp.ToDatetime() + cim.time_stamp = get_nullable(pb, 'timeStamp') # FIXME: toDatetime() ??? PBAccumulatorValue.to_cim = accumulator_value_to_cim diff --git a/src/zepben/ewb/services/network/network_service_comparator.py b/src/zepben/ewb/services/network/network_service_comparator.py index af89f9cc..4b4313d6 100644 --- a/src/zepben/ewb/services/network/network_service_comparator.py +++ b/src/zepben/ewb/services/network/network_service_comparator.py @@ -7,10 +7,12 @@ from zepben.ewb import BatteryControl, PanDemandResponseFunction, StaticVarCompensator from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.generation.production.ev_charging_unit import EvChargingUnit +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.distance_relay import DistanceRelay from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme @@ -212,6 +214,22 @@ def _compare_ev_charging_unit(self, source: EvChargingUnit, target: EvChargingUn # Extensions IEC61970 Base Protection # ####################################### + def _compare_directional_current_relay(self, source: DirectionalCurrentRelay, target: DirectionalCurrentRelay) -> ObjectDifference: + diff = ObjectDifference(source, target) + self._compare_values( + diff, + DirectionalCurrentRelay.directional_characteristic_angle, + DirectionalCurrentRelay.polarizing_quantity_type, + DirectionalCurrentRelay.relay_element_phase, + DirectionalCurrentRelay.minimum_pickup_current, + DirectionalCurrentRelay.current_limit_1, + DirectionalCurrentRelay.inverse_time_flag, + DirectionalCurrentRelay.time_delay_1, + ) + + return self._compare_protection_relay_function(diff) + + def _compare_distance_relay(self, source: DistanceRelay, target: DistanceRelay) -> ObjectDifference: diff = ObjectDifference(source, target) @@ -553,6 +571,9 @@ def _compare_usage_point(self, source: UsagePoint, target: UsagePoint) -> Object UsagePoint.approved_inverter_capacity, UsagePoint.phase_code ) + + self._compare_indexed_value_collections(diff, UsagePoint.contacts) + if self._options.compare_lv_simplification: self._compare_id_reference_collections(diff, UsagePoint.equipment) self._compare_id_reference_collections(diff, UsagePoint.end_devices) diff --git a/src/zepben/ewb/services/network/translator/network_cim2proto.py b/src/zepben/ewb/services/network/translator/network_cim2proto.py index 78fefab1..6bf0ee5a 100644 --- a/src/zepben/ewb/services/network/translator/network_cim2proto.py +++ b/src/zepben/ewb/services/network/translator/network_cim2proto.py @@ -33,12 +33,14 @@ # noinspection PyPackageRequirements,PyUnresolvedReferences from google.protobuf.timestamp_pb2 import Timestamp as PBTimestamp from zepben.protobuf.cim.extensions.iec61968.assetinfo.RelayInfo_pb2 import RelayInfo as PBRelayInfo +from zepben.protobuf.cim.extensions.iec61968.common.ContactDetails_pb2 import ContactDetails as PBContactDetails from zepben.protobuf.cim.extensions.iec61968.metering.PanDemandResponseFunction_pb2 import PanDemandResponseFunction as PBPanDemandResponseFunction from zepben.protobuf.cim.extensions.iec61970.base.core.Site_pb2 import Site as PBSite from zepben.protobuf.cim.extensions.iec61970.base.feeder.Loop_pb2 import Loop as PBLoop from zepben.protobuf.cim.extensions.iec61970.base.feeder.LvFeeder_pb2 import LvFeeder as PBLvFeeder from zepben.protobuf.cim.extensions.iec61970.base.generation.production.EvChargingUnit_pb2 import EvChargingUnit as PBEvChargingUnit from zepben.protobuf.cim.extensions.iec61970.base.protection.DistanceRelay_pb2 import DistanceRelay as PBDistanceRelay +from zepben.protobuf.cim.extensions.iec61970.base.protection.DirectionalCurrentRelay_pb2 import DirectionalCurrentRelay as PBDirectionalCurrentRelay from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelayFunction_pb2 import ProtectionRelayFunction as PBProtectionRelayFunction from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelayScheme_pb2 import ProtectionRelayScheme as PBProtectionRelayScheme from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelaySystem_pb2 import ProtectionRelaySystem as PBProtectionRelaySystem @@ -66,10 +68,12 @@ from zepben.protobuf.cim.iec61968.assets.Asset_pb2 import Asset as PBAsset from zepben.protobuf.cim.iec61968.assets.Streetlight_pb2 import Streetlight as PBStreetlight from zepben.protobuf.cim.iec61968.assets.Structure_pb2 import Structure as PBStructure +from zepben.protobuf.cim.iec61968.common.ElectronicAddress_pb2 import ElectronicAddress as PBElectronicAddress from zepben.protobuf.cim.iec61968.common.Location_pb2 import Location as PBLocation from zepben.protobuf.cim.iec61968.common.PositionPoint_pb2 import PositionPoint as PBPositionPoint from zepben.protobuf.cim.iec61968.common.StreetAddress_pb2 import StreetAddress as PBStreetAddress from zepben.protobuf.cim.iec61968.common.StreetDetail_pb2 import StreetDetail as PBStreetDetail +from zepben.protobuf.cim.iec61968.common.TelephoneNumber_pb2 import TelephoneNumber as PBTelephoneNumber from zepben.protobuf.cim.iec61968.common.TownDetail_pb2 import TownDetail as PBTownDetail from zepben.protobuf.cim.iec61968.infiec61968.infassetinfo.CurrentTransformerInfo_pb2 import CurrentTransformerInfo as PBCurrentTransformerInfo from zepben.protobuf.cim.iec61968.infiec61968.infassetinfo.PotentialTransformerInfo_pb2 import PotentialTransformerInfo as PBPotentialTransformerInfo @@ -168,7 +172,9 @@ from zepben.protobuf.cim.iec61970.infiec61970.feeder.Circuit_pb2 import Circuit as PBCircuit from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import * +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.extensions.iec61968.metering.pan_demand_reponse_function import * +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.extensions.iec61970.base.core.site import * from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import * from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import * @@ -201,10 +207,12 @@ from zepben.ewb.model.cim.iec61968.assets.asset_owner import * from zepben.ewb.model.cim.iec61968.assets.streetlight import * from zepben.ewb.model.cim.iec61968.assets.structure import * +from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.location import * from zepben.ewb.model.cim.iec61968.common.position_point import * from zepben.ewb.model.cim.iec61968.common.street_address import * from zepben.ewb.model.cim.iec61968.common.street_detail import * +from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61968.common.town_detail import * from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.current_transformer_info import * from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.potential_transformer_info import * @@ -302,14 +310,14 @@ from zepben.ewb.model.cim.iec61970.base.wires.transformer_star_impedance import * from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import * from zepben.ewb.services.common.translator.base_cim2proto import identified_object_to_pb, organisation_role_to_pb, document_to_pb, bind_to_pb, set_or_null -from zepben.ewb.services.common.translator.util import mrid_or_empty, from_nullable_int, from_nullable_float, from_nullable_long, from_nullable_uint, \ - nullable_bool_settings +from zepben.ewb.services.common.translator.util import mrid_or_empty, from_nullable_float + # noinspection PyProtectedMember from zepben.ewb.services.network.translator.network_enum_mappers import _map_battery_control_mode, _map_battery_state_kind, _map_end_device_function_kind, \ _map_feeder_direction, _map_phase_code, _map_phase_shunt_connection_kind, _map_potential_transformer_kind, _map_power_direction_kind, _map_protection_kind, \ _map_regulating_control_mode_kind, _map_single_phase_kind, _map_streetlight_lamp_kind, _map_svc_control_mode, _map_synchronous_machine_kind, \ _map_transformer_construction_kind, _map_transformer_cooling_type, _map_transformer_function_kind, _map_unit_symbol, _map_vector_group, \ - _map_winding_connection, _map_wire_material_kind + _map_winding_connection, _map_wire_material_kind, _map_contact_method_type, _map_polarizing_quantity_type def _get_or_none(getter, obj) -> Optional[Any]: @@ -336,6 +344,31 @@ def relay_info_to_pb(cim: RelayInfo) -> PBRelayInfo: ) +############################## +# Extensions IEC61968 Common # +############################## + +@bind_to_pb +def contact_details_to_pb(cim: ContactDetails) -> PBContactDetails: + pb = PBContactDetails( + id=cim.id, + contactAddress=street_address_to_pb(cim.contact_address), + preferredContactMethod=_map_contact_method_type.to_pb(cim.preferred_contact_method), + phoneNumbers=[telephone_number_to_pb(it) for it in cim.phone_numbers], + electronicAddresses=[electronic_address_to_pb(it) for it in cim.electronic_addresses], + + **set_or_null( + contactType=cim.contact_type, + firstName=cim.first_name, + lastName=cim.last_name, + isPrimary=cim.is_primary, + businessName=cim.business_name, + ) + ) + + return pb + + ################################ # Extensions IEC61968 Metering # ################################ @@ -403,6 +436,20 @@ def ev_charging_unit(cim: EvChargingUnit) -> PBEvChargingUnit: # Extensions IEC61970 Base Protection # ####################################### +@bind_to_pb +def directional_current_relay_to_pb(cim: DirectionalCurrentRelay) -> PBDirectionalCurrentRelay: + return PBDirectionalCurrentRelay( + polarizingQuantityType=_map_polarizing_quantity_type.to_pb(cim.polarizing_quantity_type), + relayElementPhase=_map_phase_code(cim.relay_element_phase), + **set_or_null( + directionalCharacteristicAngle=cim.directional_characteristic_angle, + minimumPickupCurrent=cim.minimum_pickup_current, + currentLimit1=cim.current_limit_1, + inverseTimeFlag=cim.inverse_time_flag, + timeDelay1=cim.time_delay_1, + ) + ) + @bind_to_pb def distance_relay_to_pb(cim: DistanceRelay) -> PBDistanceRelay: return PBDistanceRelay( @@ -706,6 +753,17 @@ def structure_to_pb(cim: Structure) -> PBStructure: # IEC61968 Common # ################### +@bind_to_pb +def electronic_address_to_pb(cim: ElectronicAddress) -> PBElectronicAddress: + return PBElectronicAddress( + **set_or_null( + email1=cim.email1, + isPrimary=cim.is_primary, + description=cim.description, + ) + ) + + @bind_to_pb def location_to_pb(cim: Location) -> PBLocation: return PBLocation( @@ -746,12 +804,29 @@ def street_detail_to_pb(cim: StreetDetail) -> PBStreetDetail: ) ) +@bind_to_pb +def telephone_number_to_pb(cim: TelephoneNumber) -> PBTelephoneNumber: + return PBTelephoneNumber( + **set_or_null( + areaCode=cim.area_code, + cityCode=cim.city_code, + countryCode=cim.country_code, + dialOut=cim.dial_out, + extension=cim.extension, + internationalPrefix=cim.international_prefix, + localNumber=cim.local_number, + isPrimary=cim.is_primary, + description=cim.description, + ) + ) + def town_detail_to_pb(cim: TownDetail) -> PBTownDetail: return PBTownDetail( **set_or_null( name=cim.name, - stateOrProvince=cim.state_or_province + stateOrProvince=cim.state_or_province, + country=cim.country, ) ) @@ -865,6 +940,7 @@ def usage_point_to_pb(cim: UsagePoint) -> PBUsagePoint: usagePointLocationMRID=mrid_or_empty(cim.usage_point_location), equipmentMRIDs=[str(io.mrid) for io in cim.equipment], endDeviceMRIDs=[str(io.mrid) for io in cim.end_devices], + contacts=[contact_details_to_pb(it) for it in cim.contacts], phaseCode=_map_phase_code.to_pb(cim.phase_code), **set_or_null( isVirtual=cim.is_virtual, @@ -989,7 +1065,9 @@ def equipment_to_pb(cim: Equipment, include_asset_info: bool = False) -> PBEquip usagePointMRIDs=[str(io.mrid) for io in cim.usage_points], operationalRestrictionMRIDs=[str(io.mrid) for io in cim.operational_restrictions], currentContainerMRIDs=[str(io.mrid) for io in cim.current_containers], - commissionedDate=ts, + **set_or_null( + commissionedDate=ts + ), ) diff --git a/src/zepben/ewb/services/network/translator/network_enum_mappers.py b/src/zepben/ewb/services/network/translator/network_enum_mappers.py index dce5f926..3e98957e 100644 --- a/src/zepben/ewb/services/network/translator/network_enum_mappers.py +++ b/src/zepben/ewb/services/network/translator/network_enum_mappers.py @@ -5,6 +5,8 @@ __all__ = [] +from zepben.protobuf.cim.extensions.iec61968.common.ContactMethodType_pb2 import ContactMethodType as PBContactMethodType +from zepben.protobuf.cim.extensions.iec61970.base.protection.PolarizingQuantityType_pb2 import PolarizingQuantityType as PBPolarizingQuantityType from zepben.protobuf.cim.extensions.iec61970.base.protection.PowerDirectionKind_pb2 import PowerDirectionKind as PBPowerDirectionKind from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionKind_pb2 import ProtectionKind as PBProtectionKind from zepben.protobuf.cim.extensions.iec61970.base.wires.BatteryControlMode_pb2 import BatteryControlMode as PBBatteryControlMode @@ -27,6 +29,8 @@ from zepben.protobuf.cim.iec61970.base.wires.WindingConnection_pb2 import WindingConnection as PBWindingConnection from zepben.protobuf.network.model.FeederDirection_pb2 import FeederDirection as PBFeederDirection +from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType +from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import PowerDirectionKind from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode @@ -57,10 +61,12 @@ _map_battery_control_mode = EnumMapper(BatteryControlMode, PBBatteryControlMode) _map_battery_state_kind = EnumMapper(BatteryStateKind, PBBatteryStateKind) +_map_contact_method_type = EnumMapper(ContactMethodType, PBContactMethodType) _map_end_device_function_kind = EnumMapper(EndDeviceFunctionKind, PBEndDeviceFunctionKind) _map_feeder_direction = EnumMapper(FeederDirection, PBFeederDirection) _map_phase_code = EnumMapper(PhaseCode, PBPhaseCode) _map_phase_shunt_connection_kind = EnumMapper(PhaseShuntConnectionKind, PBPhaseShuntConnectionKind) +_map_polarizing_quantity_type = EnumMapper(PolarizingQuantityType, PBPolarizingQuantityType) _map_potential_transformer_kind = EnumMapper(PotentialTransformerKind, PBPotentialTransformerKind) _map_power_direction_kind = EnumMapper(PowerDirectionKind, PBPowerDirectionKind) _map_protection_kind = EnumMapper(ProtectionKind, PBProtectionKind) diff --git a/src/zepben/ewb/services/network/translator/network_proto2cim.py b/src/zepben/ewb/services/network/translator/network_proto2cim.py index fe2f2d82..23a0914c 100644 --- a/src/zepben/ewb/services/network/translator/network_proto2cim.py +++ b/src/zepben/ewb/services/network/translator/network_proto2cim.py @@ -34,12 +34,14 @@ from typing import Optional from zepben.protobuf.cim.extensions.iec61968.assetinfo.RelayInfo_pb2 import RelayInfo as PBRelayInfo +from zepben.protobuf.cim.extensions.iec61968.common.ContactDetails_pb2 import ContactDetails as PBContactDetails from zepben.protobuf.cim.extensions.iec61968.metering.PanDemandResponseFunction_pb2 import PanDemandResponseFunction as PBPanDemandResponseFunction from zepben.protobuf.cim.extensions.iec61970.base.core.Site_pb2 import Site as PBSite from zepben.protobuf.cim.extensions.iec61970.base.feeder.Loop_pb2 import Loop as PBLoop from zepben.protobuf.cim.extensions.iec61970.base.feeder.LvFeeder_pb2 import LvFeeder as PBLvFeeder from zepben.protobuf.cim.extensions.iec61970.base.generation.production.EvChargingUnit_pb2 import EvChargingUnit as PBEvChargingUnit from zepben.protobuf.cim.extensions.iec61970.base.protection.DistanceRelay_pb2 import DistanceRelay as PBDistanceRelay +from zepben.protobuf.cim.extensions.iec61970.base.protection.DirectionalCurrentRelay_pb2 import DirectionalCurrentRelay as PBDirectionalCurrentRelay from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelayFunction_pb2 import ProtectionRelayFunction as PBProtectionRelayFunction from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelayScheme_pb2 import ProtectionRelayScheme as PBProtectionRelayScheme from zepben.protobuf.cim.extensions.iec61970.base.protection.ProtectionRelaySystem_pb2 import ProtectionRelaySystem as PBProtectionRelaySystem @@ -67,10 +69,12 @@ from zepben.protobuf.cim.iec61968.assets.Asset_pb2 import Asset as PBAsset from zepben.protobuf.cim.iec61968.assets.Streetlight_pb2 import Streetlight as PBStreetlight from zepben.protobuf.cim.iec61968.assets.Structure_pb2 import Structure as PBStructure +from zepben.protobuf.cim.iec61968.common.ElectronicAddress_pb2 import ElectronicAddress as PBElectronicAddress from zepben.protobuf.cim.iec61968.common.Location_pb2 import Location as PBLocation from zepben.protobuf.cim.iec61968.common.PositionPoint_pb2 import PositionPoint as PBPositionPoint from zepben.protobuf.cim.iec61968.common.StreetAddress_pb2 import StreetAddress as PBStreetAddress from zepben.protobuf.cim.iec61968.common.StreetDetail_pb2 import StreetDetail as PBStreetDetail +from zepben.protobuf.cim.iec61968.common.TelephoneNumber_pb2 import TelephoneNumber as PBTelephoneNumber from zepben.protobuf.cim.iec61968.common.TownDetail_pb2 import TownDetail as PBTownDetail from zepben.protobuf.cim.iec61968.infiec61968.infassetinfo.CurrentTransformerInfo_pb2 import CurrentTransformerInfo as PBCurrentTransformerInfo from zepben.protobuf.cim.iec61968.infiec61968.infassetinfo.PotentialTransformerInfo_pb2 import PotentialTransformerInfo as PBPotentialTransformerInfo @@ -169,14 +173,17 @@ from zepben.protobuf.cim.iec61970.infiec61970.feeder.Circuit_pb2 import Circuit as PBCircuit import zepben.ewb.services.common.resolver as resolver -from zepben.ewb import IdentifiedObject from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import * +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails +from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.iec61968.metering.pan_demand_reponse_function import PanDemandResponseFunction +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.extensions.iec61970.base.core.site import * from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import * from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import * from zepben.ewb.model.cim.extensions.iec61970.base.generation.production.ev_charging_unit import * from zepben.ewb.model.cim.extensions.iec61970.base.protection.distance_relay import * +from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import * from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import * from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import * @@ -210,10 +217,12 @@ from zepben.ewb.model.cim.iec61968.assets.asset_owner import * from zepben.ewb.model.cim.iec61968.assets.streetlight import * from zepben.ewb.model.cim.iec61968.assets.structure import * +from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.location import * from zepben.ewb.model.cim.iec61968.common.position_point import * from zepben.ewb.model.cim.iec61968.common.street_address import * from zepben.ewb.model.cim.iec61968.common.street_detail import * +from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61968.common.town_detail import * from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.current_transformer_info import * from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.potential_transformer_info import * @@ -326,7 +335,7 @@ from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import * from zepben.ewb.services.common.translator.base_proto2cim import identified_object_to_cim, organisation_role_to_cim, document_to_cim, add_to_network_or_none, \ bind_to_cim, get_nullable -from zepben.ewb.services.common.translator.util import int_or_none, float_or_none, long_or_none, str_or_none, uint_or_none +from zepben.ewb.services.common.translator.util import int_or_none, float_or_none, str_or_none from zepben.ewb.services.network.network_service import NetworkService from zepben.ewb.services.network.tracing.feeder.feeder_direction import FeederDirection @@ -350,6 +359,27 @@ def relay_info_to_cim(pb: PBRelayInfo, network_service: NetworkService) -> Optio asset_info_to_cim(pb.ai, cim, network_service) return cim +############################## +# Extensions IEC61968 Common # +############################## + +@bind_to_cim +def contact_details_to_cim(pb: PBContactDetails) -> ContactDetails: + cim = ContactDetails( + contact_type=get_nullable(pb, 'contactType'), + first_name=get_nullable(pb, 'firstName'), + last_name=get_nullable(pb, 'lastName'), + preferred_contact_method=ContactMethodType(pb.preferredContactMethod), + is_primary=get_nullable(pb, 'isPrimary'), + business_name=get_nullable(pb, 'businessName'), + ) + for it in pb.phoneNumbers: + cim.add_phone_number(telephone_number_to_cim(it)) + + for it in pb.electronicAddresses: + cim.add_electronic_address(electronic_address_to_cim(it)) + + return cim ################################ # Extensions IEC61968 Metering # @@ -440,6 +470,24 @@ def ev_charging_unit_to_cim(pb: PBEvChargingUnit, network_service: NetworkServic # Extensions IEC61970 Base Protection # ####################################### +@bind_to_cim +@add_to_network_or_none +def directional_current_relay_to_cim(pb: PBDirectionalCurrentRelay, network_service: NetworkService) -> DirectionalCurrentRelay | None: + cim = DirectionalCurrentRelay( + mrid=pb.mrid(), + directional_characteristic_angle=get_nullable(pb, 'directionalCharacteristicAngle'), + polarizing_quantity_type=PolarizingQuantityType(pb.polarizingQuantityType), + relay_element_phase=PhaseCode(pb.relayElementPhase), + minimum_pickup_current=get_nullable(pb, 'minimumPickupCurrent'), + current_limit_1=get_nullable(pb, 'currentLimit1'), + inverse_time_flag=get_nullable(pb, 'inverseTimeFlag'), + time_delay_1=get_nullable(pb, 'timeDelay1'), + ) + + protection_relay_function_to_cim(pb.prf, cim, network_service) + return cim + + @bind_to_cim @add_to_network_or_none def distance_relay_to_cim(pb: PBDistanceRelay, network_service: NetworkService) -> Optional[DistanceRelay]: @@ -816,6 +864,14 @@ def structure_to_cim(pb: PBStructure, cim: Structure, network_service: NetworkSe # IEC61968 Common # ################### +@bind_to_cim +def electronic_address_to_cim(pb: PBElectronicAddress, network_service: NetworkService) -> ElectronicAddress: + return ElectronicAddress( + email1=get_nullable(pb, 'email1'), + is_primary=get_nullable(pb, 'isPrimary'), + description=get_nullable(pb, 'description'), + ) + @bind_to_cim @add_to_network_or_none def location_to_cim(pb: PBLocation, network_service: NetworkService) -> Optional[Location]: @@ -853,11 +909,25 @@ def street_detail_to_cim(pb: PBStreetDetail) -> Optional[StreetDetail]: display_address=get_nullable(pb, 'displayAddress'), ) +def telephone_number_to_cim(pb: PBTelephoneNumber) -> TelephoneNumber | None: + return TelephoneNumber( + area_code=get_nullable(pb, 'areaCode'), + city_code=get_nullable(pb, 'cityCode'), + country_code=get_nullable(pb, 'countryCode'), + dial_out=get_nullable(pb, 'dialOut'), + extension=get_nullable(pb, 'extension'), + international_prefix=get_nullable(pb, 'internationalPrefix'), + local_number=get_nullable(pb, 'localNumber'), + is_primary=get_nullable(pb, 'isPrimary'), + description=get_nullable(pb, 'description'), + ) + def town_detail_to_cim(pb: PBTownDetail) -> Optional[TownDetail]: return TownDetail( name=get_nullable(pb, 'name'), state_or_province=get_nullable(pb, 'stateOrProvince'), + country=get_nullable(pb, 'country'), ) @@ -1000,6 +1070,8 @@ def usage_point_to_cim(pb: PBUsagePoint, network_service: NetworkService) -> Opt network_service.resolve_or_defer_reference(resolver.up_equipment(cim), mrid) for mrid in pb.endDeviceMRIDs: network_service.resolve_or_defer_reference(resolver.end_devices(cim), mrid) + for it in pb.contacts: + cim.add_contact(contact_details_to_cim(it)) identified_object_to_cim(pb.io, cim, network_service) return cim @@ -1129,7 +1201,7 @@ def curve_data_to_cim(pb: PBCurveData) -> Optional[CurveData]: def equipment_to_cim(pb: PBEquipment, cim: Equipment, network_service: NetworkService): cim.in_service = pb.inService cim.normally_in_service = pb.normallyInService - cim.commissioned_date = pb.commissionedDate.ToDatetime() if pb.HasField("commissionedDate") else None + cim.commissioned_date = get_nullable(pb, 'commissionedDate') for mrid in pb.equipmentContainerMRIDs: network_service.resolve_or_defer_reference(resolver.containers(cim), mrid) diff --git a/src/zepben/ewb/streaming/get/network_consumer.py b/src/zepben/ewb/streaming/get/network_consumer.py index 4c4a9820..84bdc497 100644 --- a/src/zepben/ewb/streaming/get/network_consumer.py +++ b/src/zepben/ewb/streaming/get/network_consumer.py @@ -32,6 +32,7 @@ StaticVarCompensator, PerLengthPhaseImpedance, GroundingImpedance, PetersenCoil, ReactiveCapabilityCurve, SynchronousMachine, PowerSystemResource, Asset from zepben.ewb.dataclassy import dataclass from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay from zepben.ewb.model.cim.iec61968.assetinfo.cable_info import CableInfo from zepben.ewb.model.cim.iec61968.assetinfo.overhead_wire_info import OverheadWireInfo from zepben.ewb.model.cim.iec61968.assets.asset_owner import AssetOwner @@ -702,6 +703,7 @@ def get_metadata(self) -> GrpcResult[ServiceInfo]: # Extensions IEC61970 Base Protection # ####################################### + "directionalCurrentRelay": DirectionalCurrentRelay, "distanceRelay": DistanceRelay, "protectionRelayScheme": ProtectionRelayScheme, "protectionRelaySystem": ProtectionRelaySystem, diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index e17ca143..131bafda 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -52,6 +52,9 @@ from zepben.ewb import * from hypothesis.strategies import builds, text, integers, sampled_from, booleans, uuids, datetimes, one_of, none +from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails +from zepben.ewb.model.cim.extensions.iec61970.base.protection.directional_current_relay import DirectionalCurrentRelay +from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType # @formatter:on @@ -148,19 +151,32 @@ def create_ev_charging_unit(include_runtime: bool = True): # Extensions IEC61970 Base Protection # ####################################### +def create_directional_current_relay(include_runtime: bool = True): + return builds( + DirectionalCurrentRelay, + **create_protection_relay_function(include_runtime), + directional_characteristic_angle=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + polarizing_quantity_type=one_of(none(), sampled_from(PolarizingQuantityType)), + relay_element_phase=one_of(none(), sampled_from(PhaseCode)), + minimum_pickup_current=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + current_limit_1=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + inverse_time_flag=boolean_or_none(), + time_delay_1=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + ) + def create_distance_relay(include_runtime: bool = True): return builds( DistanceRelay, **create_protection_relay_function(include_runtime), - backward_blind=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - backward_reach=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - backward_reactance=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - forward_blind=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - forward_reach=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - forward_reactance=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - operation_phase_angle1=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - operation_phase_angle2=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX), - operation_phase_angle3=floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX) + backward_blind=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + backward_reach=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + backward_reactance=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + forward_blind=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + forward_reach=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + forward_reactance=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + operation_phase_angle1=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + operation_phase_angle2=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)), + operation_phase_angle3=one_of(none(), floats(min_value=FLOAT_MIN, max_value=FLOAT_MAX)) ) @@ -493,9 +509,9 @@ def create_position_point(): def create_street_address(): return builds( StreetAddress, - postal_code=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), + postal_code=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), town_detail=create_town_detail(), - po_box=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), + po_box=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), street_detail=create_street_detail() ) @@ -503,13 +519,14 @@ def create_street_address(): def create_street_detail(): return builds( StreetDetail, - building_name=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - floor_identification=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - name=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - number=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - suite_number=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - type=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - display_address=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE) + building_name=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + floor_identification=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + name=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + number=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + suite_number=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + type=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + display_address=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)), + building_number=one_of(none(), text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)) ) @@ -517,7 +534,8 @@ def create_town_detail(): return builds( TownDetail, name=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - state_or_province=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE) + state_or_province=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), + country=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), ) @@ -680,11 +698,12 @@ def create_usage_point(include_runtime: bool = True): **create_identified_object(include_runtime), usage_point_location=builds(Location, **create_identified_object(include_runtime)), is_virtual=booleans(), - connection_category=text(alphabet=ALPHANUM, min_size=1, max_size=TEXT_MAX_SIZE), + connection_category=text(alphabet=ALPHANUM, min_size=2, max_size=TEXT_MAX_SIZE), rated_power=integers(min_value=MIN_32_BIT_INTEGER, max_value=MAX_32_BIT_INTEGER), approved_inverter_capacity=integers(min_value=MIN_32_BIT_INTEGER, max_value=MAX_32_BIT_INTEGER), equipment=lists(builds(EnergyConsumer, **create_identified_object(include_runtime)), min_size=1, max_size=2), - end_devices=lists(builds(Meter, **create_identified_object(include_runtime)), min_size=1, max_size=2) + end_devices=lists(builds(Meter, **create_identified_object(include_runtime)), min_size=1, max_size=2), + contacts=lists(builds(ContactDetails, **create_identified_object(include_runtime)), min_size=1) ) diff --git a/test/cim/iec61968/common/test_agreement.py b/test/cim/iec61968/common/test_agreement.py index 00115198..54693b2d 100644 --- a/test/cim/iec61968/common/test_agreement.py +++ b/test/cim/iec61968/common/test_agreement.py @@ -1,23 +1,38 @@ -# Copyright 2024 Zeppelin Bend Pty Ltd +# Copyright 2025 Zeppelin Bend Pty Ltd # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +import datetime + +from hypothesis.strategies import builds, datetimes, none from cim.iec61968.common.test_document import document_kwargs, verify_document_constructor_default, \ verify_document_constructor_kwargs, verify_document_constructor_args, document_args from zepben.ewb.model.cim.iec61968.common.agreement import Agreement +from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval + + +MIN_MAX = datetime.datetime(2020, 1, 1) + +agreement_kwargs = { + **document_kwargs, + 'validity_interval': builds(DateTimeInterval, start=datetimes(max_value=MIN_MAX) , end=none()), +} -agreement_kwargs = document_kwargs -agreement_args = document_args +agreement_args = [*document_args, builds(DateTimeInterval, start=datetimes(max_value=MIN_MAX), end=none())] def verify_agreement_constructor_default(a: Agreement): verify_document_constructor_default(a) -def verify_agreement_constructor_kwargs(a: Agreement, **kwargs): +def verify_agreement_constructor_kwargs(a: Agreement, validity_interval, **kwargs): + assert a.validity_interval == validity_interval verify_document_constructor_kwargs(a, **kwargs) def verify_agreement_constructor_args(a: Agreement): verify_document_constructor_args(a) + assert agreement_args[-1:] == [ + a.validity_interval + ] diff --git a/test/cim/iec61968/customers/test_customer_agreement.py b/test/cim/iec61968/customers/test_customer_agreement.py index 6ee63372..182acd99 100644 --- a/test/cim/iec61968/customers/test_customer_agreement.py +++ b/test/cim/iec61968/customers/test_customer_agreement.py @@ -45,10 +45,10 @@ def test_customer_agreement_constructor_args(): ca = CustomerAgreement(*customer_agreement_args) verify_agreement_constructor_args(ca) - assert customer_agreement_args[-2:] == [ + assert [ ca.customer, list(ca.pricing_structures) - ] + ] == customer_agreement_args[-2:] def test_pricing_structures_collection(): diff --git a/test/database/sqlite/common/cim_database_schema_common_tests.py b/test/database/sqlite/common/cim_database_schema_common_tests.py index 5a7fc0ab..3383c369 100644 --- a/test/database/sqlite/common/cim_database_schema_common_tests.py +++ b/test/database/sqlite/common/cim_database_schema_common_tests.py @@ -142,5 +142,5 @@ def _validate_service(service: BaseService, expected_service: BaseService, servi print(str(differences)) assert not list(differences.missing_from_target()), "unexpected objects found in loaded service" - assert not list(differences.modifications()), "unexpected modifications" + assert not list(differences.modifications()), ("unexpected modifications", service, expected_service) assert not list(differences.missing_from_source()), "objects missing from loaded service" diff --git a/test/database/sqlite/network/test_network_database_schema.py b/test/database/sqlite/network/test_network_database_schema.py index 85f599f9..2aae6528 100644 --- a/test/database/sqlite/network/test_network_database_schema.py +++ b/test/database/sqlite/network/test_network_database_schema.py @@ -66,6 +66,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.ratio_tap_changer import RatioTapChanger from zepben.ewb.services.common import resolver +PYTEST_TIMEOUT_SEC = 1 hypothesis_settings = dict( deadline=2000, @@ -143,6 +144,7 @@ async def test_load_real_file(self): @settings(**hypothesis_settings) @given(relay_info=create_relay_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_relay_info(self, relay_info: RelayInfo): await self._validate_schema(SchemaNetworks().network_services_of(RelayInfo, relay_info)) @@ -152,6 +154,7 @@ async def test_schema_relay_info(self, relay_info: RelayInfo): @settings(**hypothesis_settings) @given(pan_demand_response_function=create_pan_demand_response_function(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_pan_demand_response_function(self, pan_demand_response_function: PanDemandResponseFunction): await self._validate_schema(SchemaNetworks().network_services_of(PanDemandResponseFunction, pan_demand_response_function)) @@ -161,6 +164,7 @@ async def test_schema_pan_demand_response_function(self, pan_demand_response_fun @settings(**hypothesis_settings) @given(site=create_site(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_site(self, site: Site): await self._validate_schema(SchemaNetworks().network_services_of(Site, site)) @@ -170,11 +174,13 @@ async def test_schema_site(self, site: Site): @settings(**hypothesis_settings) @given(loop=create_loop(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_loop(self, loop: Loop): await self._validate_schema(SchemaNetworks().network_services_of(Loop, loop)) @settings(**hypothesis_settings) @given(lv_feeder=create_lv_feeder(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_lv_feeder(self, lv_feeder: LvFeeder): network = SchemaNetworks().network_services_of(LvFeeder, lv_feeder) await Tracing().assign_equipment_to_lv_feeders().run(network, network_state_operators=NetworkStateOperators.NORMAL) @@ -188,6 +194,7 @@ async def test_schema_lv_feeder(self, lv_feeder: LvFeeder): @settings(**hypothesis_settings) @given(ev_charging_unit=create_ev_charging_unit(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_ev_charging_unit(self, ev_charging_unit: EvChargingUnit): await self._validate_schema(SchemaNetworks().network_services_of(EvChargingUnit, ev_charging_unit)) @@ -197,21 +204,25 @@ async def test_schema_ev_charging_unit(self, ev_charging_unit: EvChargingUnit): @settings(**hypothesis_settings) @given(distance_relay=create_distance_relay(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_distance_relay(self, distance_relay: DistanceRelay): await self._validate_schema(SchemaNetworks().network_services_of(DistanceRelay, distance_relay)) @settings(**hypothesis_settings) @given(protection_relay_scheme=create_protection_relay_scheme(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_protection_relay_scheme(self, protection_relay_scheme: ProtectionRelayScheme): await self._validate_schema(SchemaNetworks().network_services_of(ProtectionRelayScheme, protection_relay_scheme)) @settings(**hypothesis_settings) @given(protection_relay_system=create_protection_relay_system(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_protection_relay_system(self, protection_relay_system: ProtectionRelaySystem): await self._validate_schema(SchemaNetworks().network_services_of(ProtectionRelaySystem, protection_relay_system)) @settings(**hypothesis_settings) @given(voltage_relay=create_voltage_relay(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_voltage_relay(self, voltage_relay: VoltageRelay): await self._validate_schema(SchemaNetworks().network_services_of(VoltageRelay, voltage_relay)) @@ -221,6 +232,7 @@ async def test_schema_voltage_relay(self, voltage_relay: VoltageRelay): @settings(**hypothesis_settings) @given(battery_control=create_battery_control(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_battery_control(self, battery_control: BatteryControl): await self._validate_schema(SchemaNetworks().network_services_of(BatteryControl, battery_control)) @@ -230,51 +242,61 @@ async def test_schema_battery_control(self, battery_control: BatteryControl): @settings(**hypothesis_settings) @given(cable_info=create_cable_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_cable_info(self, cable_info: CableInfo): await self._validate_schema(SchemaNetworks().network_services_of(CableInfo, cable_info)) @settings(**hypothesis_settings) @given(no_load_test=create_no_load_test(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_no_load_test(self, no_load_test: NoLoadTest): await self._validate_schema(SchemaNetworks().network_services_of(NoLoadTest, no_load_test)) @settings(**hypothesis_settings) @given(open_circuit_test=create_open_circuit_test(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_open_circuit_test(self, open_circuit_test: OpenCircuitTest): await self._validate_schema(SchemaNetworks().network_services_of(OpenCircuitTest, open_circuit_test)) @settings(**hypothesis_settings) @given(overhead_wire_info=create_overhead_wire_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_overhead_wire_info(self, overhead_wire_info: OverheadWireInfo): await self._validate_schema(SchemaNetworks().network_services_of(OverheadWireInfo, overhead_wire_info)) @settings(**hypothesis_settings) @given(power_transformer_info=create_power_transformer_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_transformer_info(self, power_transformer_info: PowerTransformerInfo): await self._validate_schema(SchemaNetworks().network_services_of(PowerTransformerInfo, power_transformer_info)) @settings(**hypothesis_settings) @given(short_circuit_test=create_short_circuit_test(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_short_circuit_test(self, short_circuit_test: ShortCircuitTest): await self._validate_schema(SchemaNetworks().network_services_of(ShortCircuitTest, short_circuit_test)) @settings(**hypothesis_settings) @given(shunt_compensator_info=create_shunt_compensator_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_shunt_compensator_info(self, shunt_compensator_info: ShuntCompensatorInfo): await self._validate_schema(SchemaNetworks().network_services_of(ShuntCompensatorInfo, shunt_compensator_info)) @settings(**hypothesis_settings) @given(switch_info=create_switch_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_switch_info(self, switch_info: SwitchInfo): await self._validate_schema(SchemaNetworks().network_services_of(SwitchInfo, switch_info)) @settings(**hypothesis_settings) @given(transformer_end_info=create_transformer_end_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_transformer_end_info(self, transformer_end_info: TransformerEndInfo): await self._validate_schema(SchemaNetworks().network_services_of(TransformerEndInfo, transformer_end_info)) @settings(**hypothesis_settings) @given(transformer_tank_info=create_transformer_tank_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_transformer_tank_info(self, transformer_tank_info: TransformerTankInfo): await self._validate_schema(SchemaNetworks().network_services_of(TransformerTankInfo, transformer_tank_info)) @@ -284,11 +306,13 @@ async def test_schema_transformer_tank_info(self, transformer_tank_info: Transfo @settings(**hypothesis_settings) @given(asset_owner=create_asset_owner(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_asset_owner(self, asset_owner: AssetOwner): await self._validate_schema(SchemaNetworks().network_services_of(AssetOwner, asset_owner)) @settings(**hypothesis_settings) @given(streetlight=create_streetlight(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_streetlight(self, streetlight: Streetlight): await self._validate_schema(SchemaNetworks().network_services_of(Streetlight, streetlight)) @@ -298,11 +322,13 @@ async def test_schema_streetlight(self, streetlight: Streetlight): @settings(**hypothesis_settings) @given(location=create_location(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_location(self, location: Location): await self._validate_schema(SchemaNetworks().network_services_of(Location, location)) @settings(**hypothesis_settings) @given(organisation=create_organisation(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_organisation(self, organisation: Organisation): await self._validate_schema(SchemaNetworks().network_services_of(Organisation, organisation)) @@ -312,11 +338,13 @@ async def test_schema_organisation(self, organisation: Organisation): @settings(**hypothesis_settings) @given(current_transformer_info=create_current_transformer_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_current_transformer_info(self, current_transformer_info: CurrentTransformerInfo): await self._validate_schema(SchemaNetworks().network_services_of(CurrentTransformerInfo, current_transformer_info)) @settings(**hypothesis_settings) @given(potential_transformer_info=create_potential_transformer_info(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_potential_transformer_info(self, potential_transformer_info: PotentialTransformerInfo): await self._validate_schema(SchemaNetworks().network_services_of(PotentialTransformerInfo, potential_transformer_info)) @@ -326,6 +354,7 @@ async def test_schema_potential_transformer_info(self, potential_transformer_inf @settings(**hypothesis_settings) @given(pole=create_pole(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_pole(self, pole: Pole): await self._validate_schema(SchemaNetworks().network_services_of(Pole, pole)) @@ -335,11 +364,13 @@ async def test_schema_pole(self, pole: Pole): @settings(**hypothesis_settings) @given(meter=create_meter(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_meter(self, meter: Meter): await self._validate_schema(SchemaNetworks().network_services_of(Meter, meter)) @settings(**hypothesis_settings) @given(usage_point=create_usage_point(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_usage_point(self, usage_point: UsagePoint): await self._validate_schema(SchemaNetworks().network_services_of(UsagePoint, usage_point)) @@ -349,6 +380,7 @@ async def test_schema_usage_point(self, usage_point: UsagePoint): @settings(**hypothesis_settings) @given(operational_restriction=create_operational_restriction(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_operational_restriction(self, operational_restriction: OperationalRestriction): await self._validate_schema(SchemaNetworks().network_services_of(OperationalRestriction, operational_restriction)) @@ -358,16 +390,19 @@ async def test_schema_operational_restriction(self, operational_restriction: Ope @settings(**hypothesis_settings) @given(current_transformer=create_current_transformer(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_current_transformer(self, current_transformer: CurrentTransformer): await self._validate_schema(SchemaNetworks().network_services_of(CurrentTransformer, current_transformer)) @settings(**hypothesis_settings) @given(fault_indicator=create_fault_indicator(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_fault_indicator(self, fault_indicator: FaultIndicator): await self._validate_schema(SchemaNetworks().network_services_of(FaultIndicator, fault_indicator)) @settings(**hypothesis_settings) @given(potential_transformer=create_potential_transformer(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_potential_transformer(self, potential_transformer: PotentialTransformer): await self._validate_schema(SchemaNetworks().network_services_of(PotentialTransformer, potential_transformer)) @@ -377,16 +412,19 @@ async def test_schema_potential_transformer(self, potential_transformer: Potenti @settings(**hypothesis_settings) @given(base_voltage=create_base_voltage(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_base_voltage(self, base_voltage: BaseVoltage): await self._validate_schema(SchemaNetworks().network_services_of(BaseVoltage, base_voltage)) @settings(**hypothesis_settings) @given(connectivity_node=create_connectivity_node(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_connectivity_node(self, connectivity_node: ConnectivityNode): await self._validate_schema(SchemaNetworks().network_services_of(ConnectivityNode, connectivity_node)) @settings(**hypothesis_settings) @given(feeder=create_feeder(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_feeder(self, feeder: Feeder): # Need to set feeder directions to match database load. network_service = SchemaNetworks().network_services_of(Feeder, feeder) @@ -409,21 +447,25 @@ async def test_schema_feeder(self, feeder: Feeder): @settings(**hypothesis_settings) @given(geographical_region=create_geographical_region(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_geographical_region(self, geographical_region: GeographicalRegion): await self._validate_schema(SchemaNetworks().network_services_of(GeographicalRegion, geographical_region)) @settings(**hypothesis_settings) @given(sub_geographical_region=create_sub_geographical_region(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion): await self._validate_schema(SchemaNetworks().network_services_of(SubGeographicalRegion, sub_geographical_region)) @settings(**hypothesis_settings) @given(substation=create_substation(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_substation(self, substation: Substation): await self._validate_schema(SchemaNetworks().network_services_of(Substation, substation)) @settings(**hypothesis_settings) @given(terminal=create_terminal(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_terminal(self, terminal: Terminal): await self._validate_schema(SchemaNetworks().network_services_of(Terminal, terminal)) @@ -433,6 +475,7 @@ async def test_schema_terminal(self, terminal: Terminal): @settings(**hypothesis_settings) @given(equivalent_branch=create_equivalent_branch(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_equivalent_branch(self, equivalent_branch: EquivalentBranch): await self._validate_schema(SchemaNetworks().network_services_of(EquivalentBranch, equivalent_branch)) @@ -442,16 +485,19 @@ async def test_schema_equivalent_branch(self, equivalent_branch: EquivalentBranc @settings(**hypothesis_settings) @given(battery_unit=create_battery_unit(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_battery_unit(self, battery_unit: BatteryUnit): await self._validate_schema(SchemaNetworks().network_services_of(BatteryUnit, battery_unit)) @settings(**hypothesis_settings) @given(photo_voltaic_unit=create_photo_voltaic_unit(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_photo_voltaic_unit(self, photo_voltaic_unit: PhotoVoltaicUnit): await self._validate_schema(SchemaNetworks().network_services_of(PhotoVoltaicUnit, photo_voltaic_unit)) @settings(**hypothesis_settings) @given(power_electronics_wind_unit=create_power_electronics_wind_unit(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_electronics_wind_unit(self, power_electronics_wind_unit: PowerElectronicsWindUnit): await self._validate_schema(SchemaNetworks().network_services_of(PowerElectronicsWindUnit, power_electronics_wind_unit)) @@ -461,21 +507,25 @@ async def test_schema_power_electronics_wind_unit(self, power_electronics_wind_u @settings(**hypothesis_settings) @given(accumulator=create_accumulator(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_accumulator(self, accumulator: Accumulator): await self._validate_schema(SchemaNetworks().network_services_of(Accumulator, accumulator)) @settings(**hypothesis_settings) @given(analog=create_analog(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_analog(self, analog: Analog): await self._validate_schema(SchemaNetworks().network_services_of(Analog, analog)) @settings(**hypothesis_settings) @given(control=create_control(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_control(self, control: Control): await self._validate_schema(SchemaNetworks().network_services_of(Control, control)) @settings(**hypothesis_settings) @given(discrete=create_discrete(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_discrete(self, discrete: Discrete): await self._validate_schema(SchemaNetworks().network_services_of(Discrete, discrete)) @@ -485,6 +535,7 @@ async def test_schema_discrete(self, discrete: Discrete): @settings(**hypothesis_settings) @given(current_relay=create_current_relay(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_current_relay(self, current_relay: CurrentRelay): await self._validate_schema(SchemaNetworks().network_services_of(CurrentRelay, current_relay)) @@ -494,11 +545,13 @@ async def test_schema_current_relay(self, current_relay: CurrentRelay): @settings(**hypothesis_settings) @given(remote_control=create_remote_control(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_remote_control(self, remote_control: RemoteControl): await self._validate_schema(SchemaNetworks().network_services_of(RemoteControl, remote_control)) @settings(**hypothesis_settings) @given(remote_source=create_remote_source(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_remote_source(self, remote_source: RemoteSource): await self._validate_schema(SchemaNetworks().network_services_of(RemoteSource, remote_source)) @@ -508,36 +561,43 @@ async def test_schema_remote_source(self, remote_source: RemoteSource): @settings(**hypothesis_settings) @given(ac_line_segment=create_ac_line_segment(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_ac_line_segment(self, ac_line_segment: AcLineSegment): await self._validate_schema(SchemaNetworks().network_services_of(AcLineSegment, ac_line_segment)) @settings(**hypothesis_settings) @given(breaker=create_breaker(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_breaker(self, breaker: Breaker): await self._validate_schema(SchemaNetworks().network_services_of(Breaker, breaker)) @settings(**hypothesis_settings) @given(busbar_section=create_busbar_section(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_busbar_section(self, busbar_section: BusbarSection): await self._validate_schema(SchemaNetworks().network_services_of(BusbarSection, busbar_section)) @settings(**hypothesis_settings) @given(clamp=create_clamp(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_clamp(self, clamp: Clamp): await self._validate_schema(SchemaNetworks().network_services_of(Clamp, clamp)) @settings(**hypothesis_settings) @given(cut=create_cut(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_cut(self, cut: Cut): await self._validate_schema(SchemaNetworks().network_services_of(Cut, cut)) @settings(**hypothesis_settings) @given(disconnector=create_disconnector(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_disconnector(self, disconnector: Disconnector): await self._validate_schema(SchemaNetworks().network_services_of(Disconnector, disconnector)) @settings(**hypothesis_settings) @given(energy_consumer=create_energy_consumer(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_energy_consumer(self, energy_consumer: EnergyConsumer): # Need to assure the correct number of phases to prevent errors. assume(len(Counter(map(lambda it: it.phase, energy_consumer.phases))) == len(list(energy_consumer.phases))) @@ -545,11 +605,13 @@ async def test_schema_energy_consumer(self, energy_consumer: EnergyConsumer): @settings(**hypothesis_settings) @given(energy_consumer_phase=create_energy_consumer_phase(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_energy_consumer_phase(self, energy_consumer_phase: EnergyConsumerPhase): await self._validate_schema(SchemaNetworks().network_services_of(EnergyConsumerPhase, energy_consumer_phase)) @settings(**hypothesis_settings) @given(energy_source=create_energy_source(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_energy_source(self, energy_source: EnergySource): # Need to assure the correct number of phases to prevent errors. assume(len(Counter(map(lambda it: it.phase, energy_source.phases))) == len(list(energy_source.phases))) @@ -562,121 +624,145 @@ async def test_schema_energy_source(self, energy_source: EnergySource): @settings(**hypothesis_settings) @given(energy_source_phase=create_energy_source_phase(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_energy_source_phase(self, energy_source_phase: EnergyConsumerPhase): await self._validate_schema(SchemaNetworks().network_services_of(EnergySourcePhase, energy_source_phase)) @settings(**hypothesis_settings) @given(fuse=create_fuse(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_fuse(self, fuse: Fuse): await self._validate_schema(SchemaNetworks().network_services_of(Fuse, fuse)) @settings(**hypothesis_settings) @given(ground=create_ground(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_ground(self, ground: Ground): await self._validate_schema(SchemaNetworks().network_services_of(Ground, ground)) @settings(**hypothesis_settings) @given(ground_disconnector=create_ground_disconnector(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_ground_disconnector(self, ground_disconnector: GroundDisconnector): await self._validate_schema(SchemaNetworks().network_services_of(GroundDisconnector, ground_disconnector)) @settings(**hypothesis_settings) @given(grounding_impedance=create_grounding_impedance(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_grounding_impedance(self, grounding_impedance: GroundingImpedance): await self._validate_schema(SchemaNetworks().network_services_of(GroundingImpedance, grounding_impedance)) @settings(**hypothesis_settings) @given(jumper=create_jumper(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_jumper(self, jumper: Jumper): await self._validate_schema(SchemaNetworks().network_services_of(Jumper, jumper)) @settings(**hypothesis_settings) @given(junction=create_junction(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_junction(self, junction: Junction): await self._validate_schema(SchemaNetworks().network_services_of(Junction, junction)) @settings(**hypothesis_settings) @given(linear_shunt_compensator=create_linear_shunt_compensator(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_linear_shunt_compensator(self, linear_shunt_compensator: LinearShuntCompensator): await self._validate_schema(SchemaNetworks().network_services_of(LinearShuntCompensator, linear_shunt_compensator)) @settings(**hypothesis_settings) @given(load_break_switch=create_load_break_switch(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_load_break_switch(self, load_break_switch: LoadBreakSwitch): await self._validate_schema(SchemaNetworks().network_services_of(LoadBreakSwitch, load_break_switch)) @settings(**hypothesis_settings) @given(per_length_phase_impedance=create_per_length_phase_impedance(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_per_length_phase_impedance(self, per_length_phase_impedance: PerLengthPhaseImpedance): await self._validate_schema(SchemaNetworks().network_services_of(PerLengthPhaseImpedance, per_length_phase_impedance)) @settings(**hypothesis_settings) @given(per_length_sequence_impedance=create_per_length_sequence_impedance(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_per_length_sequence_impedance(self, per_length_sequence_impedance: PerLengthSequenceImpedance): await self._validate_schema(SchemaNetworks().network_services_of(PerLengthSequenceImpedance, per_length_sequence_impedance)) @settings(**hypothesis_settings) @given(petersen_coil=create_petersen_coil(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_petersen_coil(self, petersen_coil: PetersenCoil): await self._validate_schema(SchemaNetworks().network_services_of(PetersenCoil, petersen_coil)) @settings(**hypothesis_settings) @given(power_electronics_connection=create_power_electronics_connection(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_electronics_connection(self, power_electronics_connection: PowerElectronicsConnection): await self._validate_schema(SchemaNetworks().network_services_of(PowerElectronicsConnection, power_electronics_connection)) @settings(**hypothesis_settings) @given(power_electronics_connection_phase=create_power_electronics_connection_phase(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_electronics_connection_phase(self, power_electronics_connection_phase: PowerElectronicsConnectionPhase): await self._validate_schema(SchemaNetworks().network_services_of(PowerElectronicsConnectionPhase, power_electronics_connection_phase)) @settings(**hypothesis_settings) @given(power_transformer=create_power_transformer(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_transformer(self, power_transformer: PowerTransformer): await self._validate_schema(SchemaNetworks().network_services_of(PowerTransformer, power_transformer)) @settings(**hypothesis_settings) @given(power_transformer_end=create_power_transformer_end(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_power_transformer_end(self, power_transformer_end: PowerTransformerEnd): await self._validate_schema(SchemaNetworks().network_services_of(PowerTransformerEnd, power_transformer_end)) @settings(**hypothesis_settings) @given(ratio_tap_changer=create_ratio_tap_changer(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_ratio_tap_changer(self, ratio_tap_changer: RatioTapChanger): await self._validate_schema(SchemaNetworks().network_services_of(RatioTapChanger, ratio_tap_changer)) @settings(**hypothesis_settings) @given(reactive_capability_curve=create_reactive_capability_curve(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_reactive_capability_curve(self, reactive_capability_curve: ReactiveCapabilityCurve): await self._validate_schema(SchemaNetworks().network_services_of(ReactiveCapabilityCurve, reactive_capability_curve)) @settings(**hypothesis_settings) @given(recloser=create_recloser(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_recloser(self, recloser: Recloser): await self._validate_schema(SchemaNetworks().network_services_of(Recloser, recloser)) @settings(**hypothesis_settings) @given(series_compensator=create_series_compensator(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_series_compensator(self, series_compensator: SeriesCompensator): await self._validate_schema(SchemaNetworks().network_services_of(SeriesCompensator, series_compensator)) @settings(**hypothesis_settings) @given(static_var_compensator=create_static_var_compensator(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_static_var_compensator(self, static_var_compensator: StaticVarCompensator): await self._validate_schema(SchemaNetworks().network_services_of(StaticVarCompensator, static_var_compensator)) @settings(**hypothesis_settings) @given(synchronous_machine=create_synchronous_machine(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_synchronous_machine(self, synchronous_machine: SynchronousMachine): await self._validate_schema(SchemaNetworks().network_services_of(SynchronousMachine, synchronous_machine)) @settings(**hypothesis_settings) @given(tap_changer_control=create_tap_changer_control(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_tap_changer_control(self, tap_changer_control: TapChangerControl): await self._validate_schema(SchemaNetworks().network_services_of(TapChangerControl, tap_changer_control)) @settings(**hypothesis_settings) @given(transformer_star_impedance=create_transformer_star_impedance(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_transformer_star_impedance(self, transformer_star_impedance: TransformerStarImpedance): await self._validate_schema(SchemaNetworks().network_services_of(TransformerStarImpedance, transformer_star_impedance)) @@ -686,14 +772,17 @@ async def test_schema_transformer_star_impedance(self, transformer_star_impedanc @settings(**hypothesis_settings) @given(circuit=create_circuit(False)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_schema_circuit(self, circuit: Circuit): await self._validate_schema(SchemaNetworks().network_services_of(Circuit, circuit)) # ************ Services ************ + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_name_and_name_type_schema(self): await self._validate_schema(SchemaNetworks().create_name_test_services(NetworkService, Junction)) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_post_process_fails_with_unresolved_references(self): pec = PowerElectronicsConnection(mrid="pec1") @@ -702,6 +791,7 @@ def add_deferred_reference(service: NetworkService): await self._validate_unresolved_failure(str(pec), "RegulatingControl tcc", add_deferred_reference) + @pytest.mark.timeout(PYTEST_TIMEOUT_SEC) async def test_only_loads_street_address_fields_if_required(self): # This test is here to make sure the database reading correctly removes the parts of loaded street addresses that are not filled out. write_service = NetworkService() diff --git a/test/services/network/translator/test_network_translator.py b/test/services/network/translator/test_network_translator.py index a160d5b3..6e815bca 100644 --- a/test/services/network/translator/test_network_translator.py +++ b/test/services/network/translator/test_network_translator.py @@ -6,6 +6,7 @@ import pytest +from cim.cim_creators import create_directional_current_relay from services.common.translator.base_test_translator import validate_service_translations from test.cim.cim_creators import * from zepben.ewb import IdentifiedObject, PowerTransformerEnd, PowerTransformer, NetworkService, NetworkServiceComparator, NameType, \ @@ -55,6 +56,7 @@ # Extensions IEC61970 Base Protection # ####################################### + "create_directional_current_relay": create_directional_current_relay(), "create_distance_relay": create_distance_relay(), "create_protection_relay_scheme": create_protection_relay_scheme(), "create_protection_relay_system": create_protection_relay_system(), diff --git a/test/streaming/get/pb_creators.py b/test/streaming/get/pb_creators.py index 905a79fb..e3dbf7a1 100644 --- a/test/streaming/get/pb_creators.py +++ b/test/streaming/get/pb_creators.py @@ -555,7 +555,7 @@ def document(): PBDocument, io=identified_object(), titleSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - createdDateTime=timestamp(), + createdDateTimeSet=timestamp(), authorNameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), typeSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), statusSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), @@ -592,18 +592,23 @@ def street_address(): def street_detail(): return builds( PBStreetDetail, - buildingNameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - floorIdentificationSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - nameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - numberSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - suiteNumberSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - typeSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - displayAddressSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE) + buildingNameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + floorIdentificationSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + nameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + numberSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + suiteNumberSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + typeSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + displayAddressSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + buildingNumberSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str) ) def town_detail(): - return builds(PBTownDetail, nameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), stateOrProvinceSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE)) + return builds( + PBTownDetail, + nameSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str), + stateOrProvinceSet=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE).map(str) + ) ###################### @@ -829,7 +834,7 @@ def equipment(): psr=power_system_resource(), inService=booleans(), normallyInService=booleans(), - commissionedDate=timestamp(), + commissionedDateSet=timestamp(), equipmentContainerMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2), usagePointMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2), operationalRestrictionMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2),