From dd0653cee0bf846eefae9572badf1b7da0b28b5e Mon Sep 17 00:00:00 2001 From: Kurt Greaves Date: Thu, 24 Apr 2025 15:00:40 +1000 Subject: [PATCH 1/5] [DEV-2391] Support Asset -> PowerSystemResource relationship Signed-off-by: Kurt Greaves --- src/zepben/evolve/__init__.py | 3 + .../sqlite/network/network_cim_reader.py | 31 ++++++++ .../sqlite/network/network_cim_writer.py | 13 +++- .../sqlite/network/network_database_tables.py | 2 + .../sqlite/network/network_service_reader.py | 2 + .../table_assets_power_system_resources.py | 41 +++++++++++ .../database/sqlite/tables/table_version.py | 2 +- .../evolve/model/cim/iec61968/assets/asset.py | 70 +++++++++++++++++-- .../base/core/power_system_resource.py | 68 +++++++++++++++++- .../iec61970/base/wires/regulating_control.py | 2 +- .../services/common/reference_resolvers.py | 5 +- src/zepben/evolve/services/common/resolver.py | 10 +++ .../network/network_service_comparator.py | 2 + .../network/translator/network_cim2proto.py | 6 +- .../network/translator/network_proto2cim.py | 6 ++ test/cim/cim_creators.py | 6 +- test/cim/iec61968/assets/test_asset.py | 29 ++++++-- .../base/core/test_power_system_resource.py | 33 +++++++-- test/database/sqlite/schema_utils.py | 6 ++ .../test_network_service_comparator.py | 14 ++++ .../translator/test_network_translator.py | 6 +- test/streaming/get/pb_creators.py | 6 +- 22 files changed, 330 insertions(+), 33 deletions(-) create mode 100644 src/zepben/evolve/database/sqlite/tables/associations/table_assets_power_system_resources.py diff --git a/src/zepben/evolve/__init__.py b/src/zepben/evolve/__init__.py index ee345997a..c702df075 100644 --- a/src/zepben/evolve/__init__.py +++ b/src/zepben/evolve/__init__.py @@ -253,6 +253,7 @@ from zepben.evolve.database.sqlite.tables.table_version import * from zepben.evolve.database.sqlite.tables.associations.loop_substation_relationship import * from zepben.evolve.database.sqlite.tables.associations.table_asset_organisation_roles_assets import * +from zepben.evolve.database.sqlite.tables.associations.table_assets_power_system_resources import * from zepben.evolve.database.sqlite.tables.associations.table_battery_units_battery_controls import * from zepben.evolve.database.sqlite.tables.associations.table_end_devices_end_device_functions import * from zepben.evolve.database.sqlite.tables.associations.table_circuits_substations import * @@ -266,6 +267,7 @@ from zepben.evolve.database.sqlite.tables.associations.table_protection_relay_functions_protected_switches import * from zepben.evolve.database.sqlite.tables.associations.table_protection_relay_functions_sensors import * from zepben.evolve.database.sqlite.tables.associations.table_protection_relay_schemes_protection_relay_functions import * +from zepben.evolve.database.sqlite.tables.associations.table_synchronous_machines_reactive_capability_curves import * from zepben.evolve.database.sqlite.tables.associations.table_usage_points_end_devices import * from zepben.evolve.database.sqlite.tables.iec61968.assetinfo.table_cable_info import * from zepben.evolve.database.sqlite.tables.iec61968.assetinfo.table_no_load_tests import * @@ -320,6 +322,7 @@ from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_conducting_equipment import * from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_connectivity_node_containers import * from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_connectivity_nodes import * +from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_curve_data import * from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_equipment import * from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_equipment_containers import * from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_feeders import * diff --git a/src/zepben/evolve/database/sqlite/network/network_cim_reader.py b/src/zepben/evolve/database/sqlite/network/network_cim_reader.py index 2316d178b..e00bbe5c1 100644 --- a/src/zepben/evolve/database/sqlite/network/network_cim_reader.py +++ b/src/zepben/evolve/database/sqlite/network/network_cim_reader.py @@ -55,6 +55,7 @@ from zepben.evolve.database.sqlite.extensions.result_set import ResultSet from zepben.evolve.database.sqlite.tables.associations.loop_substation_relationship import LoopSubstationRelationship from zepben.evolve.database.sqlite.tables.associations.table_asset_organisation_roles_assets import TableAssetOrganisationRolesAssets +from zepben.evolve.database.sqlite.tables.associations.table_assets_power_system_resources import TableAssetsPowerSystemResources from zepben.evolve.database.sqlite.tables.associations.table_circuits_substations import TableCircuitsSubstations from zepben.evolve.database.sqlite.tables.associations.table_circuits_terminals import TableCircuitsTerminals from zepben.evolve.database.sqlite.tables.associations.table_equipment_equipment_containers import TableEquipmentEquipmentContainers @@ -2718,6 +2719,36 @@ def load_asset_organisation_roles_asset( return True + def load_asset_power_system_resources( + self, + table: TableAssetsPowerSystemResources, + result_set: ResultSet, + set_identifier: Callable[[str], str] + ) -> bool: + """ + Create a :class:`Asset` to :class:`PowerSystemResource` association from :class:`TableAssetPowerSystemResources`. + + :param table: The database table to read the association from. + :param result_set: The record in the database table containing the fields for this association. + :param set_identifier: A callback to register the identifier of this association for logging purposes. + + :return: True if the association was successfully read from the database and added to the service. + :raises SqlException: For any errors encountered reading from the database. + """ + asset_mrid = result_set.get_string(table.asset_mrid.query_index) + set_identifier(f"{asset_mrid}-to-UNKNOWN") + + power_system_resource_mrid = result_set.get_string(table.power_system_resource_mrid.query_index) + set_identifier(f"{asset_mrid}-to-{power_system_resource_mrid}") + + asset = self._service.get(asset_mrid, Asset) + power_system_resource = self._service.get(power_system_resource_mrid, PowerSystemResource) + + asset.add_power_system_resource(power_system_resource) + power_system_resource.add_asset(asset) + + return True + def load_battery_units_battery_controls(self, table: TableBatteryUnitsBatteryControls, result_set: ResultSet, set_identifier: Callable[[str], str]) -> bool: """ Create a :class:`BatteryUnit` to :class:`BatteryControl` association from :class:`TableBatteryUnitsBatteryControls`. diff --git a/src/zepben/evolve/database/sqlite/network/network_cim_writer.py b/src/zepben/evolve/database/sqlite/network/network_cim_writer.py index fc0ab792c..231fa73bf 100644 --- a/src/zepben/evolve/database/sqlite/network/network_cim_writer.py +++ b/src/zepben/evolve/database/sqlite/network/network_cim_writer.py @@ -9,6 +9,7 @@ from zepben.evolve.database.sqlite.network.network_database_tables import NetworkDatabaseTables from zepben.evolve.database.sqlite.tables.associations.loop_substation_relationship import LoopSubstationRelationship from zepben.evolve.database.sqlite.tables.associations.table_asset_organisation_roles_assets import TableAssetOrganisationRolesAssets +from zepben.evolve.database.sqlite.tables.associations.table_assets_power_system_resources import TableAssetsPowerSystemResources from zepben.evolve.database.sqlite.tables.associations.table_battery_units_battery_controls import TableBatteryUnitsBatteryControls from zepben.evolve.database.sqlite.tables.associations.table_circuits_substations import TableCircuitsSubstations from zepben.evolve.database.sqlite.tables.associations.table_circuits_terminals import TableCircuitsTerminals @@ -518,6 +519,8 @@ def _save_asset(self, table: TableAssets, insert: PreparedStatement, asset: Asse insert.add_value(table.location_mrid.query_index, self._mrid_or_none(asset.location)) for it in asset.organisation_roles: status = status and self._save_asset_organisation_role_to_asset_association(it, asset) + for it in asset.power_system_resources: + status = status and self._save_asset_to_power_system_resource_association(it, asset) return status and self._save_identified_object(table, insert, asset, description) @@ -2357,6 +2360,15 @@ def _save_asset_organisation_role_to_asset_association(self, asset_organisation_ return self._try_execute_single_update(insert, "asset organisation role to asset association") + def _save_asset_to_power_system_resource_association(self, power_system_resource: PowerSystemResource, asset: Asset) -> bool: + table = self._database_tables.get_table(TableAssetsPowerSystemResources) + insert = self._database_tables.get_insert(TableAssetsPowerSystemResources) + + insert.add_value(table.asset_mrid.query_index, asset.mrid) + insert.add_value(table.power_system_resource_mrid.query_index, power_system_resource.mrid) + + return self._try_execute_single_update(insert, "asset to power system resource association") + def _save_battery_unit_to_battery_control_association(self, battery_unit: BatteryUnit, battery_control: BatteryControl) -> bool: table = self._database_tables.get_table(TableBatteryUnitsBatteryControls) insert = self._database_tables.get_insert(TableBatteryUnitsBatteryControls) @@ -2393,7 +2405,6 @@ def _save_end_device_function_to_end_device_association(self, end_device_functio return self._try_execute_single_update(insert, "end device function to end device association") - def _save_equipment_to_equipment_container_association(self, equipment: Equipment, equipment_container: EquipmentContainer) -> bool: table = self._database_tables.get_table(TableEquipmentEquipmentContainers) insert = self._database_tables.get_insert(TableEquipmentEquipmentContainers) diff --git a/src/zepben/evolve/database/sqlite/network/network_database_tables.py b/src/zepben/evolve/database/sqlite/network/network_database_tables.py index 22c69d498..3a3259f4e 100644 --- a/src/zepben/evolve/database/sqlite/network/network_database_tables.py +++ b/src/zepben/evolve/database/sqlite/network/network_database_tables.py @@ -6,6 +6,7 @@ from zepben.evolve.database.sqlite.common.base_database_tables import BaseDatabaseTables from zepben.evolve.database.sqlite.tables.associations.table_asset_organisation_roles_assets import * +from zepben.evolve.database.sqlite.tables.associations.table_assets_power_system_resources import TableAssetsPowerSystemResources from zepben.evolve.database.sqlite.tables.associations.table_battery_units_battery_controls import * from zepben.evolve.database.sqlite.tables.associations.table_circuits_substations import * from zepben.evolve.database.sqlite.tables.associations.table_circuits_terminals import * @@ -133,6 +134,7 @@ def _included_tables(self) -> Generator[SqliteTable, None, None]: yield TableAccumulators() yield TableAnalogs() yield TableAssetOrganisationRolesAssets() + yield TableAssetsPowerSystemResources() yield TableAssetOwners() yield TableBaseVoltages() yield TableBatteryControls() diff --git a/src/zepben/evolve/database/sqlite/network/network_service_reader.py b/src/zepben/evolve/database/sqlite/network/network_service_reader.py index dac609345..f1520595e 100644 --- a/src/zepben/evolve/database/sqlite/network/network_service_reader.py +++ b/src/zepben/evolve/database/sqlite/network/network_service_reader.py @@ -11,6 +11,7 @@ from zepben.evolve.database.sqlite.network.network_cim_reader import NetworkCimReader from zepben.evolve.database.sqlite.network.network_database_tables import NetworkDatabaseTables from zepben.evolve.database.sqlite.tables.associations.table_asset_organisation_roles_assets import TableAssetOrganisationRolesAssets +from zepben.evolve.database.sqlite.tables.associations.table_assets_power_system_resources import TableAssetsPowerSystemResources from zepben.evolve.database.sqlite.tables.associations.table_battery_units_battery_controls import TableBatteryUnitsBatteryControls from zepben.evolve.database.sqlite.tables.associations.table_circuits_substations import TableCircuitsSubstations from zepben.evolve.database.sqlite.tables.associations.table_circuits_terminals import TableCircuitsTerminals @@ -241,6 +242,7 @@ def _do_load(self) -> bool: self._load_each(TablePositionPoints, self._reader.load_position_point), self._load_each(TableLocationStreetAddresses, self._reader.load_location_street_address), self._load_each(TableAssetOrganisationRolesAssets, self._reader.load_asset_organisation_roles_asset), + self._load_each(TableAssetsPowerSystemResources, self._reader.load_asset_power_system_resources), self._load_each(TableUsagePointsEndDevices, self._reader.load_usage_points_end_device), self._load_each(TableEquipmentUsagePoints, self._reader.load_equipment_usage_point), self._load_each(TableEquipmentOperationalRestrictions, self._reader.load_equipment_operational_restriction), diff --git a/src/zepben/evolve/database/sqlite/tables/associations/table_assets_power_system_resources.py b/src/zepben/evolve/database/sqlite/tables/associations/table_assets_power_system_resources.py new file mode 100644 index 000000000..8f5e0849d --- /dev/null +++ b/src/zepben/evolve/database/sqlite/tables/associations/table_assets_power_system_resources.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/. + +from typing import List, Generator + +from zepben.evolve.database.sqlite.tables.column import Column, Nullable +from zepben.evolve.database.sqlite.tables.sqlite_table import SqliteTable + +__all__ = ["TableAssetsPowerSystemResources"] + + +class TableAssetsPowerSystemResources(SqliteTable): + """ + A class representing the association between AssetOrganisationRoles and Assets. + """ + + def __init__(self): + super().__init__() + + self.asset_mrid: Column = self._create_column("asset_mrid", "TEXT", Nullable.NOT_NULL) + """A column storing the mRID of Assets.""" + + self.power_system_resource_mrid: Column = self._create_column("power_system_resource_mrid", "TEXT", Nullable.NOT_NULL) + """A column storing the mRID of PowerSystemResources.""" + + @property + def name(self) -> str: + return "assets_power_system_resources" + + @property + def unique_index_columns(self) -> Generator[List[Column], None, None]: + yield from super().unique_index_columns + yield [self.asset_mrid, self.power_system_resource_mrid] + + @property + def non_unique_index_columns(self) -> Generator[List[Column], None, None]: + yield from super().non_unique_index_columns + yield [self.asset_mrid] + yield [self.power_system_resource_mrid] diff --git a/src/zepben/evolve/database/sqlite/tables/table_version.py b/src/zepben/evolve/database/sqlite/tables/table_version.py index b4baa52f6..106548aae 100644 --- a/src/zepben/evolve/database/sqlite/tables/table_version.py +++ b/src/zepben/evolve/database/sqlite/tables/table_version.py @@ -14,7 +14,7 @@ class TableVersion(SqliteTable): - SUPPORTED_VERSION = 58 + SUPPORTED_VERSION = 59 def __init__(self): self.version: Column = self._create_column("version", "TEXT", Nullable.NOT_NULL) diff --git a/src/zepben/evolve/model/cim/iec61968/assets/asset.py b/src/zepben/evolve/model/cim/iec61968/assets/asset.py index 3219cefc2..cf678aae6 100644 --- a/src/zepben/evolve/model/cim/iec61968/assets/asset.py +++ b/src/zepben/evolve/model/cim/iec61968/assets/asset.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: from zepben.evolve import AssetOrganisationRole + from zepben.evolve import PowerSystemResource from zepben.evolve.model.cim.iec61968.common.location import Location from zepben.evolve.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -30,12 +31,18 @@ class Asset(IdentifiedObject): _organisation_roles: Optional[List[AssetOrganisationRole]] = None - def __init__(self, organisation_roles: List[AssetOrganisationRole] = None, **kwargs): + _power_system_resources: Optional[List[PowerSystemResource]] = None + + def __init__(self, organisation_roles: List[AssetOrganisationRole] = None, power_system_resources: List[PowerSystemResource] = None, **kwargs): super(Asset, self).__init__(**kwargs) if organisation_roles: for role in organisation_roles: self.add_organisation_role(role) + if power_system_resources: + for resource in power_system_resources: + self.add_power_system_resource(resource) + def num_organisation_roles(self) -> int: """ Get the number of `AssetOrganisationRole`s associated with this `Asset`. @@ -61,8 +68,7 @@ def get_organisation_role(self, mrid: str) -> AssetOrganisationRole: def add_organisation_role(self, role: AssetOrganisationRole) -> Asset: """ - `role` The `AssetOrganisationRole` to - associate with this `Asset`. + `role` The `AssetOrganisationRole` to associate with this `Asset`. Returns A reference to this `Asset` to allow fluent use. Raises `ValueError` if another `AssetOrganisationRole` with the same `mrid` already exists in this `Asset` """ @@ -77,8 +83,7 @@ def remove_organisation_role(self, role: AssetOrganisationRole) -> Asset: """ Disassociate an `AssetOrganisationRole` from this `Asset`. - `role` the `AssetOrganisationRole` to - disassociate with this `Asset`. + `role` the `AssetOrganisationRole` to disassociate from this `Asset`. Raises `ValueError` if `role` was not associated with this `Asset`. Returns A reference to this `Asset` to allow fluent use. """ @@ -93,6 +98,61 @@ def clear_organisation_roles(self) -> Asset: self._organisation_roles = None return self + def num_power_system_resources(self) -> int: + """ + Get the number of `PowerSystemResource`s associated with this `Asset`. + """ + return nlen(self._power_system_resources) + + @property + def power_system_resources(self) -> Generator[PowerSystemResource, None, None]: + """ + The `PowerSystemResource`s of this `Asset`. + """ + return ngen(self._power_system_resources) + + def get_power_system_resource(self, mrid: str) -> PowerSystemResource: + """ + Get the `PowerSystemResource` for this asset identified by `mrid`. + + `mrid` the mRID of the required `PowerSystemResource` + Returns The `PowerSystemResource` with the specified `mrid`. + Raises `KeyError` if `mrid` wasn't present. + """ + return get_by_mrid(self._power_system_resources, mrid) + + def add_power_system_resource(self, resource: PowerSystemResource) -> Asset: + """ + `resource` The `PowerSystemResource` to associate with this `Asset`. + Returns A reference to this `Asset` to allow fluent use. + Raises `ValueError` if another `PowerSystemResource` with the same `mrid` already exists in this `Asset` + """ + if self._validate_reference(resource, self.get_power_system_resource, "An PowerSystemResource"): + return self + + self._power_system_resources = list() if self._power_system_resources is None else self._power_system_resources + self._power_system_resources.append(resource) + return self + + def remove_power_system_resource(self, resource: PowerSystemResource) -> Asset: + """ + Disassociate an `PowerSystemResource` from this `Asset`. + + `resource` the `PowerSystemResource` to disassociate from this `Asset`. + Raises `ValueError` if `resource` was not associated with this `Asset`. + Returns A reference to this `Asset` to allow fluent use. + """ + self._power_system_resources = safe_remove(self._power_system_resources, resource) + return self + + def clear_power_system_resources(self) -> Asset: + """ + Clear all power system resources. + Returns self + """ + self._power_system_resources = None + return self + class AssetContainer(Asset): """ diff --git a/src/zepben/evolve/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/evolve/model/cim/iec61970/base/core/power_system_resource.py index b282ab815..71550ae18 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/evolve/model/cim/iec61970/base/core/power_system_resource.py @@ -5,11 +5,14 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING, List, Generator, Iterable if TYPE_CHECKING: from zepben.evolve.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.evolve.model.cim.iec61968.common.location import Location + from zepben.evolve import Asset + +from zepben.evolve.util import get_by_mrid, nlen, ngen, safe_remove from zepben.evolve.model.cim.iec61970.base.core.identified_object import IdentifiedObject __all__ = ['PowerSystemResource'] @@ -32,9 +35,72 @@ class PowerSystemResource(IdentifiedObject): num_controls: int = 0 """Number of Control's known to associate with this [PowerSystemResource]""" + _assets: Optional[List[Asset]] = None + + def __init__(self, assets: Iterable[Asset] = None, **kwargs): + super(PowerSystemResource, self).__init__(**kwargs) + if assets: + for asset in assets: + self.add_asset(asset) + @property def has_controls(self) -> bool: """ * :return: True if this [PowerSystemResource] has at least 1 Control associated with it, false otherwise. """ return self.num_controls > 0 + + def num_assets(self) -> int: + """ + Get the number of `Asset`s associated with this `PowerSystemResource`. + """ + return nlen(self._assets) + + @property + def assets(self) -> Generator[Asset, None, None]: + """ + The `Asset`s of this `PowerSystemResource`. + """ + return ngen(self._assets) + + def get_asset(self, mrid: str) -> Asset: + """ + Get the `Asset` associated with this `PowerSystemResource` identified by `mrid`. + + `mrid` the mRID of the required `Asset` + Returns The `Asset` with the specified `mrid`. + Raises `KeyError` if `mrid` wasn't present. + """ + return get_by_mrid(self._assets, mrid) + + def add_asset(self, asset: Asset) -> PowerSystemResource: + """ + `asset` The `Asset` to associate with this `PowerSystemResource`. + Returns A reference to this `PowerSystemResource` to allow fluent use. + Raises `ValueError` if another `Asset` with the same `mrid` already exists in this `PowerSystemResource` + """ + if self._validate_reference(asset, self.get_asset, "An Asset"): + return self + + self._assets = list() if self._assets is None else self._assets + self._assets.append(asset) + return self + + def remove_asset(self, asset: Asset) -> PowerSystemResource: + """ + Disassociate an `Asset` from this `PowerSystemResource`. + + `asset` the `Asset` to disassociate from this `PowerSystemResource`. + Raises `ValueError` if `asset` was not associated with this `PowerSystemResource`. + Returns A reference to this `PowerSystemResource` to allow fluent use. + """ + self._assets = safe_remove(self._assets, asset) + return self + + def clear_assets(self) -> PowerSystemResource: + """ + Clear all assets. + Returns self + """ + self._assets = None + return self diff --git a/src/zepben/evolve/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/evolve/model/cim/iec61970/base/wires/regulating_control.py index 9781bfd5a..0ca063a15 100644 --- a/src/zepben/evolve/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/evolve/model/cim/iec61970/base/wires/regulating_control.py @@ -108,7 +108,7 @@ class RegulatingControl(PowerSystemResource): """The [RegulatingCondEq] that are controlled by this regulating control scheme.""" def __init__(self, regulating_conducting_equipment: Optional[Iterable[RegulatingCondEq]] = None, **kwargs): - super(PowerSystemResource, self).__init__(**kwargs) + super(RegulatingControl, self).__init__(**kwargs) if regulating_conducting_equipment is not None: for eq in regulating_conducting_equipment: self.add_regulating_cond_eq(eq) diff --git a/src/zepben/evolve/services/common/reference_resolvers.py b/src/zepben/evolve/services/common/reference_resolvers.py index 43a6d84aa..62a525a7a 100644 --- a/src/zepben/evolve/services/common/reference_resolvers.py +++ b/src/zepben/evolve/services/common/reference_resolvers.py @@ -101,7 +101,7 @@ "psw_to_prf_resolver", "prf_to_sen_resolver", "sen_to_prf_resolver", "prf_to_prscheme_resolver", "prscheme_to_prf_resolver", "prscheme_to_prsystem_resolver", "battery_unit_to_battery_control_resolver", "ed_to_edf_resolver", "prsystem_to_prscheme_resolver", "fuse_to_prf_resolver", "sm_to_rcc_resolver", "feeder_to_celvf_resolver", "lvfeeder_to_cef_resolver", "acls_to_cut_resolver", "cut_to_acls_resolver", "acls_to_clamp_resolver", - "clamp_to_acls_resolver"] + "clamp_to_acls_resolver", "asset_to_psr_resolver", "psr_to_asset_resolver"] @dataclass(frozen=True, eq=False, slots=True) @@ -356,3 +356,6 @@ def _resolve_diag_diagobj(diag, diag_obj): battery_unit_to_battery_control_resolver = ReferenceResolver(BatteryUnit, BatteryControl, lambda t, r: t.add_control(r)) ed_to_edf_resolver = ReferenceResolver(EndDevice, EndDeviceFunction, lambda t, r: t.add_function(r)) + +asset_to_psr_resolver = ReferenceResolver(Asset, PowerSystemResource, lambda t, r: t.add_power_system_resource(r)) +psr_to_asset_resolver = ReferenceResolver(PowerSystemResource, Asset, lambda t, r: t.add_asset(r)) diff --git a/src/zepben/evolve/services/common/resolver.py b/src/zepben/evolve/services/common/resolver.py index 29f07ad58..cc8cab964 100644 --- a/src/zepben/evolve/services/common/resolver.py +++ b/src/zepben/evolve/services/common/resolver.py @@ -46,6 +46,11 @@ def agreements(c: Customer) -> BoundReferenceResolver: return BoundReferenceResolver(c, cust_to_custagr_resolver, custagr_to_cust_resolver) +def assets(power_system_resource: PowerSystemResource) -> BoundReferenceResolver: + # noinspection PyArgumentList + return BoundReferenceResolver(power_system_resource, psr_to_asset_resolver, asset_to_psr_resolver) + + def at_location(asset: Asset) -> BoundReferenceResolver: # noinspection PyArgumentList return BoundReferenceResolver(asset, asset_to_location_resolver, None) @@ -376,6 +381,11 @@ def power_transformer_info_transformer_tank_info(pti: PowerTransformerInfo) -> B return BoundReferenceResolver(pti, pti_to_tti_resolver, None) +def power_system_resources(asset: Asset) -> BoundReferenceResolver: + # noinspection PyArgumentList + return BoundReferenceResolver(asset, asset_to_psr_resolver, psr_to_asset_resolver) + + def prf_protected_switch(prf: ProtectionRelayFunction) -> BoundReferenceResolver: # noinspection PyArgumentList return BoundReferenceResolver(prf, prf_to_psw_resolver, psw_to_prf_resolver) diff --git a/src/zepben/evolve/services/network/network_service_comparator.py b/src/zepben/evolve/services/network/network_service_comparator.py index 569bf9e56..55524fe3c 100644 --- a/src/zepben/evolve/services/network/network_service_comparator.py +++ b/src/zepben/evolve/services/network/network_service_comparator.py @@ -279,6 +279,7 @@ def _compare_wire_info(self, diff: ObjectDifference) -> ObjectDifference: def _compare_asset(self, diff: ObjectDifference) -> ObjectDifference: self._compare_id_references(diff, Asset.location) self._compare_id_reference_collections(diff, Asset.organisation_roles) + self._compare_id_reference_collections(diff, Asset.power_system_resources) return self._compare_identified_object(diff) @@ -542,6 +543,7 @@ def _compare_geographical_region(self, source: GeographicalRegion, target: Geogr def _compare_power_system_resource(self, diff: ObjectDifference) -> ObjectDifference: self._compare_id_references(diff, PowerSystemResource.asset_info, PowerSystemResource.location) + self._compare_id_reference_collections(diff, PowerSystemResource.assets) return self._compare_identified_object(diff) diff --git a/src/zepben/evolve/services/network/translator/network_cim2proto.py b/src/zepben/evolve/services/network/translator/network_cim2proto.py index 37fbc8537..5af9e8e57 100644 --- a/src/zepben/evolve/services/network/translator/network_cim2proto.py +++ b/src/zepben/evolve/services/network/translator/network_cim2proto.py @@ -480,7 +480,8 @@ def asset_to_pb(cim: Asset) -> PBAsset: return PBAsset( io=identified_object_to_pb(cim), locationMRID=cim.location.mrid if cim.location else None, - organisationRoleMRIDs=[str(io.mrid) for io in cim.organisation_roles] + organisationRoleMRIDs=[str(io.mrid) for io in cim.organisation_roles], + powerSystemResourceMRIDs=[str(io.mrid) for io in cim.power_system_resources] ) @@ -831,7 +832,8 @@ def power_system_resource_to_pb(cim: PowerSystemResource, include_asset_info: bo return PBPowerSystemResource( io=identified_object_to_pb(cim), assetInfoMRID=mrid_or_empty(cim.asset_info) if include_asset_info else None, - locationMRID=mrid_or_empty(cim.location) + locationMRID=mrid_or_empty(cim.location), + assetMRIDs=[str(io.mrid) for io in cim.assets] ) diff --git a/src/zepben/evolve/services/network/translator/network_proto2cim.py b/src/zepben/evolve/services/network/translator/network_proto2cim.py index 3348b5211..b4540608d 100644 --- a/src/zepben/evolve/services/network/translator/network_proto2cim.py +++ b/src/zepben/evolve/services/network/translator/network_proto2cim.py @@ -514,6 +514,9 @@ def asset_to_cim(pb: PBAsset, cim: Asset, network_service: NetworkService): for mrid in pb.organisationRoleMRIDs: network_service.resolve_or_defer_reference(resolver.organisation_roles(cim), mrid) + for mrid in pb.powerSystemResourceMRIDs: + network_service.resolve_or_defer_reference(resolver.power_system_resources(cim), mrid) + identified_object_to_cim(pb.io, cim, network_service) @@ -924,6 +927,9 @@ def geographical_region_to_cim(pb: PBGeographicalRegion, network_service: Networ def power_system_resource_to_cim(pb: PBPowerSystemResource, cim: PowerSystemResource, network_service: NetworkService): network_service.resolve_or_defer_reference(resolver.psr_location(cim), pb.locationMRID) + for mrid in pb.assetMRIDs: + network_service.resolve_or_defer_reference(resolver.assets(cim), mrid) + identified_object_to_cim(pb.io, cim, network_service) diff --git a/test/cim/cim_creators.py b/test/cim/cim_creators.py index 103f48220..7518fbd01 100644 --- a/test/cim/cim_creators.py +++ b/test/cim/cim_creators.py @@ -236,7 +236,8 @@ def create_asset(include_runtime: bool): return { **create_identified_object(include_runtime), "location": builds(Location, **create_identified_object(include_runtime)), - "organisation_roles": lists(builds(AssetOwner, **create_identified_object(include_runtime)), min_size=1, max_size=2) + "organisation_roles": lists(builds(AssetOwner, **create_identified_object(include_runtime)), min_size=1, max_size=2), + "power_system_resources": lists(builds(Junction, **create_identified_object(include_runtime)), min_size=1, max_size=2) } @@ -706,7 +707,8 @@ def create_power_system_resource(include_runtime: bool): # return { **create_identified_object(include_runtime), - "location": create_location() + "location": create_location(), + "assets": lists(builds(Pole, **create_identified_object(include_runtime)), min_size=1, max_size=2) } diff --git a/test/cim/iec61968/assets/test_asset.py b/test/cim/iec61968/assets/test_asset.py index 5558a58d3..f3a7481df 100644 --- a/test/cim/iec61968/assets/test_asset.py +++ b/test/cim/iec61968/assets/test_asset.py @@ -4,7 +4,7 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from hypothesis.strategies import builds, lists -from zepben.evolve import Asset, Location, AssetOrganisationRole +from zepben.evolve import Asset, Location, AssetOrganisationRole, PowerSystemResource from cim.iec61970.base.core.test_identified_object import identified_object_kwargs, verify_identified_object_constructor_default, \ verify_identified_object_constructor_kwargs, verify_identified_object_constructor_args, identified_object_args @@ -13,29 +13,33 @@ asset_kwargs = { **identified_object_kwargs, "location": builds(Location), - "organisation_roles": lists(builds(AssetOrganisationRole), max_size=2) + "organisation_roles": lists(builds(AssetOrganisationRole), max_size=2), + "power_system_resources": lists(builds(PowerSystemResource), max_size=2) } -asset_args = [*identified_object_args, Location(), [AssetOrganisationRole()]] +asset_args = [*identified_object_args, Location(), [AssetOrganisationRole()], [PowerSystemResource()]] def verify_asset_constructor_default(a: Asset): verify_identified_object_constructor_default(a) assert not a.location assert not list(a.organisation_roles) + assert not list(a.power_system_resources) -def verify_asset_constructor_kwargs(a: Asset, location, organisation_roles, **kwargs): +def verify_asset_constructor_kwargs(a: Asset, location, organisation_roles, power_system_resources, **kwargs): verify_identified_object_constructor_kwargs(a, **kwargs) assert a.location == location assert list(a.organisation_roles) == organisation_roles + assert list(a.power_system_resources) == power_system_resources def verify_asset_constructor_args(a: Asset): verify_identified_object_constructor_args(a) - assert asset_args[-2:] == [ + assert asset_args[-3:] == [ a.location, - list(a.organisation_roles) + list(a.organisation_roles), + list(a.power_system_resources) ] @@ -50,3 +54,16 @@ def test_organisation_roles_collection(): Asset.remove_organisation_role, Asset.clear_organisation_roles ) + + +def test_power_system_resources_collection(): + validate_unordered_1234567890( + Asset, + lambda mrid: PowerSystemResource(mrid), + Asset.power_system_resources, + Asset.num_power_system_resources, + Asset.get_power_system_resource, + Asset.add_power_system_resource, + Asset.remove_power_system_resource, + Asset.clear_power_system_resources + ) diff --git a/test/cim/iec61970/base/core/test_power_system_resource.py b/test/cim/iec61970/base/core/test_power_system_resource.py index 310c39245..f3d30a898 100644 --- a/test/cim/iec61970/base/core/test_power_system_resource.py +++ b/test/cim/iec61970/base/core/test_power_system_resource.py @@ -2,8 +2,10 @@ # 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 hypothesis.strategies import builds, integers -from zepben.evolve import PowerSystemResource, Location, PowerTransformerInfo +from hypothesis.strategies import builds, integers, lists + +from cim.private_collection_validator import validate_unordered_1234567890 +from zepben.evolve import PowerSystemResource, Location, PowerTransformerInfo, Asset from cim.cim_creators import sampled_wire_info, MIN_32_BIT_INTEGER, MAX_32_BIT_INTEGER from cim.iec61970.base.core.test_identified_object import identified_object_kwargs, verify_identified_object_constructor_default, \ @@ -13,10 +15,11 @@ **identified_object_kwargs, "location": builds(Location), "asset_info": sampled_wire_info(True), - "num_controls": integers(min_value=MIN_32_BIT_INTEGER, max_value=MAX_32_BIT_INTEGER) + "num_controls": integers(min_value=MIN_32_BIT_INTEGER, max_value=MAX_32_BIT_INTEGER), + "assets": lists(builds(Asset), max_size=2) } -power_system_resource_args = [*identified_object_args, Location(), PowerTransformerInfo(), 1] +power_system_resource_args = [*identified_object_args, Location(), PowerTransformerInfo(), 1, [Asset()]] def verify_power_system_resource_constructor_default(psr: PowerSystemResource): @@ -24,19 +27,35 @@ def verify_power_system_resource_constructor_default(psr: PowerSystemResource): assert psr.location is None assert psr.asset_info is None assert psr.num_controls == 0 + assert not list(psr.assets) -def verify_power_system_resource_constructor_kwargs(psr: PowerSystemResource, location, asset_info, num_controls, **kwargs): +def verify_power_system_resource_constructor_kwargs(psr: PowerSystemResource, location, asset_info, num_controls, assets, **kwargs): verify_identified_object_constructor_kwargs(psr, **kwargs) assert psr.location is location assert psr.asset_info is asset_info assert psr.num_controls == num_controls + assert list(psr.assets) == assets def verify_power_system_resource_constructor_args(psr: PowerSystemResource): verify_identified_object_constructor_args(psr) - assert power_system_resource_args[-3:] == [ + assert power_system_resource_args[-4:] == [ psr.location, psr.asset_info, - psr.num_controls + psr.num_controls, + list(psr.assets) ] + + +def test_assets_collection(): + validate_unordered_1234567890( + PowerSystemResource, + lambda mrid: Asset(mrid), + PowerSystemResource.assets, + PowerSystemResource.num_assets, + PowerSystemResource.get_asset, + PowerSystemResource.add_asset, + PowerSystemResource.remove_asset, + PowerSystemResource.clear_assets + ) diff --git a/test/database/sqlite/schema_utils.py b/test/database/sqlite/schema_utils.py index 6378a6415..b4c435cc0 100644 --- a/test/database/sqlite/schema_utils.py +++ b/test/database/sqlite/schema_utils.py @@ -143,6 +143,9 @@ def _add_with_references(filled: T, service: BaseService): service.add(filled.location) for it in filled.organisation_roles: service.add(it) + for it in filled.power_system_resources: + service.add(it) + it.add_asset(filled) if isinstance(filled, Pole): for it in filled.streetlights: @@ -272,6 +275,9 @@ def _add_with_references(filled: T, service: BaseService): if isinstance(filled, PowerSystemResource): service.add(filled.location) + for it in filled.assets: + it.add_power_system_resource(filled) + service.add(it) if isinstance(filled, SubGeographicalRegion): filled.geographical_region.add_sub_geographical_region(filled) diff --git a/test/services/network/test_network_service_comparator.py b/test/services/network/test_network_service_comparator.py index 8b613823a..05ec2acd9 100644 --- a/test/services/network/test_network_service_comparator.py +++ b/test/services/network/test_network_service_comparator.py @@ -184,6 +184,13 @@ def _compare_asset(self, creator: Type[Asset]): lambda _: AssetOwner(mrid="a1"), lambda _: AssetOwner(mrid="a2") ) + self.validator.validate_collection( + Asset.power_system_resources, + Asset.add_power_system_resource, + creator, + lambda _: Junction(mrid="j1"), + lambda _: Junction(mrid="j2") + ) self.validator.validate_property(Asset.location, creator, lambda _: Location(mrid="l1"), lambda _: Location(mrid="l2")) def _compare_asset_container(self, creator: Type[AssetContainer]): @@ -575,6 +582,13 @@ def _compare_power_system_resource(self, creator: Type[PowerSystemResource]): self._compare_identified_object(creator) self.validator.validate_property(PowerSystemResource.location, creator, lambda _: Location(mrid="l1"), lambda _: Location(mrid="l2")) + self.validator.validate_collection( + PowerSystemResource.assets, + PowerSystemResource.add_asset, + creator, + lambda _: Pole(mrid="p1"), + lambda _: Pole(mrid="p2") + ) def test_compare_site(self): self._compare_equipment_container(Site) diff --git a/test/services/network/translator/test_network_translator.py b/test/services/network/translator/test_network_translator.py index 909c41918..53efee651 100644 --- a/test/services/network/translator/test_network_translator.py +++ b/test/services/network/translator/test_network_translator.py @@ -16,10 +16,7 @@ TableProtectionRelayFunctionsProtectedSwitches, TableProtectionRelaySchemesProtectionRelayFunctions, TableUsagePointsEndDevices, \ TableLocationStreetAddresses, TablePositionPoints, TablePowerTransformerEndRatings, TableProtectionRelayFunctionThresholds, \ TableProtectionRelayFunctionTimeLimits, TableProtectionRelayFunctionsSensors, TableRecloseDelays, TablePhaseImpedanceData, TableBatteryUnitsBatteryControls, \ - TableEndDevicesEndDeviceFunctions -from zepben.evolve.database.sqlite.tables.associations.table_synchronous_machines_reactive_capability_curves import \ - TableSynchronousMachinesReactiveCapabilityCurves -from zepben.evolve.database.sqlite.tables.iec61970.base.core.table_curve_data import TableCurveData + TableEndDevicesEndDeviceFunctions, TableAssetsPowerSystemResources, TableSynchronousMachinesReactiveCapabilityCurves, TableCurveData T = TypeVar("T", bound=IdentifiedObject) @@ -220,6 +217,7 @@ def test_network_service_translations(**kwargs): excluded_tables={ # Excluded associations. TableAssetOrganisationRolesAssets, + TableAssetsPowerSystemResources, TableBatteryUnitsBatteryControls, TableCircuitsSubstations, TableCircuitsTerminals, diff --git a/test/streaming/get/pb_creators.py b/test/streaming/get/pb_creators.py index 81779482a..91bbff070 100644 --- a/test/streaming/get/pb_creators.py +++ b/test/streaming/get/pb_creators.py @@ -366,7 +366,8 @@ def asset(): PBAsset, io=identified_object(), locationMRID=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - organisationRoleMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2) + organisationRoleMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2), + powerSystemResourceMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2) ) @@ -720,7 +721,8 @@ def power_system_resource(): PBPowerSystemResource, io=identified_object(), assetInfoMRID=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), - locationMRID=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE) + locationMRID=text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), + assetMRIDs=lists(text(alphabet=ALPHANUM, max_size=TEXT_MAX_SIZE), max_size=2) ) From d2b2763e39289b624eda7286865dac98657c4e3d Mon Sep 17 00:00:00 2001 From: Kurt Greaves Date: Thu, 24 Apr 2025 15:01:40 +1000 Subject: [PATCH 2/5] Update changelog Signed-off-by: Kurt Greaves --- changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 7bb836ec4..c16d8ae55 100644 --- a/changelog.md +++ b/changelog.md @@ -4,7 +4,9 @@ * None. ### New Features -* None. +* Added relationships between `Asset` and `PowerSystemResource` which enables linking `Equipment` to `Pole`: + * `Asset.powerSystemResources` + * `PowerSystemResource.assets` ### Enhancements * None. From 591b26314354c9bbf240faa62ceb9a2f4c59dd5e Mon Sep 17 00:00:00 2001 From: Kurt Greaves Date: Thu, 24 Apr 2025 15:20:11 +1000 Subject: [PATCH 3/5] Update to latest proto Signed-off-by: Kurt Greaves --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f163fc282..67b8abcf3 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ deps = [ "zepben.auth==0.12.1", - "zepben.protobuf==0.34.1", + "zepben.protobuf==0.35.0b2", "typing_extensions==4.12.2", ] From f39714db003e23efde76ef33a043c58f828afe4d Mon Sep 17 00:00:00 2001 From: Kurt Greaves Date: Thu, 24 Apr 2025 16:00:10 +1000 Subject: [PATCH 4/5] Support latest gRPC testing lib Signed-off-by: Kurt Greaves --- setup.py | 2 +- .../get/grpcio_aio_testing/mock_async_channel.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 67b8abcf3..ad552d17f 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ "pytest-asyncio==0.19.0", "pytest-timeout==1.4.2", "hypothesis==6.56.3", - "grpcio-testing==1.57.0", + "grpcio-testing==1.71.0", "pylint==2.14.5", "six==1.16.0", "tox" diff --git a/test/streaming/get/grpcio_aio_testing/mock_async_channel.py b/test/streaming/get/grpcio_aio_testing/mock_async_channel.py index 3b008595c..77fae1a82 100644 --- a/test/streaming/get/grpcio_aio_testing/mock_async_channel.py +++ b/test/streaming/get/grpcio_aio_testing/mock_async_channel.py @@ -125,25 +125,33 @@ def unsubscribe(self, callback): def unary_unary(self, method, request_serializer=None, - response_deserializer=None): + response_deserializer=None, + _registered_method=None, + ): return UnaryUnary(method, self._state) def unary_stream(self, method, request_serializer=None, - response_deserializer=None): + response_deserializer=None, + _registered_method=None, + ): return UnaryStream(method, self._state) def stream_unary(self, method, request_serializer=None, - response_deserializer=None): + response_deserializer=None, + _registered_method=None, + ): return StreamUnary(method, self._state) def stream_stream(self, method, request_serializer=None, - response_deserializer=None): + response_deserializer=None, + _registered_method=None, + ): return StreamStream(method, self._state) def _close(self): From 11e1c5280eb433043a75153391f9b72e89f60748 Mon Sep 17 00:00:00 2001 From: Kurt Greaves Date: Thu, 24 Apr 2025 16:25:12 +1000 Subject: [PATCH 5/5] Update to released grpc Signed-off-by: Kurt Greaves --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ad552d17f..bc168036f 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ deps = [ "zepben.auth==0.12.1", - "zepben.protobuf==0.35.0b2", + "zepben.protobuf==0.35.0", "typing_extensions==4.12.2", ]