From 28e6eaf7cf3571298dd75e47e85dbd21791e6857 Mon Sep 17 00:00:00 2001 From: Alex Vollebergh Date: Tue, 7 Jul 2026 11:52:26 +1000 Subject: [PATCH 1/5] Standardise configs for examples and remove redundancy Signed-off-by: Alex Vollebergh --- src/zepben/examples/__init__.py | 5 +++-- .../examples/cim/extract_hv_customers.py | 2 +- .../examples/cim/extract_lv_substations.py | 2 +- .../cim/extract_per_phase_wire_info.py | 2 +- .../examples/element_network_hierarchy.py | 11 +++++++---- src/zepben/examples/id_csv_generator.py | 4 ++-- .../request_feeder_load_analysis_study.py | 8 ++++---- .../examples/request_network_model_ingest.py | 8 ++++---- src/zepben/examples/sample_auth_config.json | 11 ----------- src/zepben/examples/sample_config.json | 10 ++++++---- src/zepben/examples/tx_id_to_name.py | 4 ++-- src/zepben/examples/utils.py | 18 +++++++++--------- 12 files changed, 40 insertions(+), 45 deletions(-) delete mode 100644 src/zepben/examples/sample_auth_config.json diff --git a/src/zepben/examples/__init__.py b/src/zepben/examples/__init__.py index 2db2a76..b207483 100644 --- a/src/zepben/examples/__init__.py +++ b/src/zepben/examples/__init__.py @@ -5,5 +5,6 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from pathlib import Path -# root dir of repo if cloned -CONFIG_DIR = f"{Path(__file__).parent.parent.parent.parent}/" +# Directory this package lives in, i.e. src/zepben/examples, which is where config.json +# is expected to live (see utils.py::get_client and the various scripts that open "config.json"). +CONFIG_DIR = f"{Path(__file__).parent}/" diff --git a/src/zepben/examples/cim/extract_hv_customers.py b/src/zepben/examples/cim/extract_hv_customers.py index fa7b8b3..3ffcf74 100644 --- a/src/zepben/examples/cim/extract_hv_customers.py +++ b/src/zepben/examples/cim/extract_hv_customers.py @@ -15,7 +15,7 @@ async def extract_hv_customers_for_feeder(feeder_mrid: str): - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c["ca_path"]) + channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c.get("ca_filename")) network_client = NetworkConsumerClient(channel=channel) network = network_client.service diff --git a/src/zepben/examples/cim/extract_lv_substations.py b/src/zepben/examples/cim/extract_lv_substations.py index 8f8c4a0..514dee2 100644 --- a/src/zepben/examples/cim/extract_lv_substations.py +++ b/src/zepben/examples/cim/extract_lv_substations.py @@ -15,7 +15,7 @@ async def extract_lv_substations_for_feeder(feeder_mrid: str): - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c["ca_path"]) + channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c.get("ca_filename")) network_client = NetworkConsumerClient(channel=channel) network = network_client.service diff --git a/src/zepben/examples/cim/extract_per_phase_wire_info.py b/src/zepben/examples/cim/extract_per_phase_wire_info.py index 27dc801..80b4933 100644 --- a/src/zepben/examples/cim/extract_per_phase_wire_info.py +++ b/src/zepben/examples/cim/extract_per_phase_wire_info.py @@ -15,7 +15,7 @@ async def extract_wire_info_per_phase(feeder_mrid: str): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c["ca_path"]) + channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) network_client = NetworkConsumerClient(channel=channel) network = network_client.service (await network_client.get_equipment_container(feeder_mrid, diff --git a/src/zepben/examples/element_network_hierarchy.py b/src/zepben/examples/element_network_hierarchy.py index 5082ecb..651f3d4 100644 --- a/src/zepben/examples/element_network_hierarchy.py +++ b/src/zepben/examples/element_network_hierarchy.py @@ -6,8 +6,9 @@ """ For every piece of `ConductingEquipment` downstream of a feeder, records a row with its equipment type, -nominal voltage, length (where applicable), and the nearest upstream fuse, distribution transformer, -regulator and breaker mrid — so an `element_id` can be related back to its measurement zones. +nominal voltage, length (where applicable), and the nearest upstream protection device (fuse or LV +circuit breaker), distribution transformer, regulator and breaker mrid — so an `element_id` can be +related back to its measurement zones. """ import asyncio import json @@ -35,7 +36,7 @@ def _build_row(equip: ConductingEquipment, upstream: dict) -> dict: 'element_type': type(equip).__name__, 'nominal_voltage': equip.base_voltage_value, 'length': getattr(equip, 'length', None) if isinstance(equip, Conductor) else None, - 'upstream_fuse_mrid': getattr(upstream.get('upstream_fuse'), 'mrid', None), + 'upstream_protection_device_mrid': getattr(upstream.get('upstream_protection_device'), 'mrid', None), 'distribution_power_transformer_mrid': getattr(upstream.get('distribution_power_transformer'), 'mrid', None), 'regulator_mrid': getattr(upstream.get('regulator'), 'mrid', None), 'breaker_mrid': getattr(upstream.get('breaker'), 'mrid', None), @@ -63,8 +64,10 @@ def compute_next_value(self, next_item: NetworkTraceStep, current_item: NetworkT equip = next_item.path.to_equipment if isinstance(equip, Breaker): upstream['breaker'] = equip + if equip.base_voltage_value is not None and equip.base_voltage_value <= 1000: + upstream['upstream_protection_device'] = equip elif isinstance(equip, Fuse): - upstream['upstream_fuse'] = equip + upstream['upstream_protection_device'] = equip elif isinstance(equip, PowerTransformer): if equip.function == TransformerFunctionKind.distributionTransformer: upstream['distribution_power_transformer'] = equip diff --git a/src/zepben/examples/id_csv_generator.py b/src/zepben/examples/id_csv_generator.py index 0664611..65d4497 100644 --- a/src/zepben/examples/id_csv_generator.py +++ b/src/zepben/examples/id_csv_generator.py @@ -16,7 +16,7 @@ from zepben.ewb import NetworkConsumerClient, connect_with_token, ConductingEquipment, Feeder, IncludedEnergizedContainers -with open("./config.json") as f: +with open("config.json") as f: c = json.loads(f.read()) """ @@ -26,7 +26,7 @@ async def connect(): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c["ca_path"]) + channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) network_client = NetworkConsumerClient(channel=channel) network_hierarchy = (await network_client.get_network_hierarchy()).throw_on_error().value diff --git a/src/zepben/examples/request_feeder_load_analysis_study.py b/src/zepben/examples/request_feeder_load_analysis_study.py index 089e8b5..575ea84 100644 --- a/src/zepben/examples/request_feeder_load_analysis_study.py +++ b/src/zepben/examples/request_feeder_load_analysis_study.py @@ -17,12 +17,12 @@ async def main(argv): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EAS..") eas_client = EasClient( - host=c["eas_host"], - port=c["eas_port"], - protocol=c["eas_protocol"], + host=c["host"], + port=c["rpc_port"], + protocol="https", access_token=c["access_token"], verify_certificate=c.get("verify_certificate", True), - ca_filename=c["ca_path"], + ca_filename=c.get("ca_filename"), asynchronous=True ) print("Connection established..") diff --git a/src/zepben/examples/request_network_model_ingest.py b/src/zepben/examples/request_network_model_ingest.py index 3d30914..650c01a 100644 --- a/src/zepben/examples/request_network_model_ingest.py +++ b/src/zepben/examples/request_network_model_ingest.py @@ -20,12 +20,12 @@ async def main(argv): print("Connecting to EAS..") eas_client = EasClient( - host=c["eas_host"], - port=c["eas_port"], - protocol=c["eas_protocol"], + host=c["host"], + port=c["rpc_port"], + protocol="https", access_token=c["access_token"], verify_certificate=c.get("verify_certificate", True), - ca_filename=c["ca_path"], + ca_filename=c.get("ca_filename"), asynchronous=True ) print("Connection established..") diff --git a/src/zepben/examples/sample_auth_config.json b/src/zepben/examples/sample_auth_config.json deleted file mode 100644 index 824a871..0000000 --- a/src/zepben/examples/sample_auth_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "eas_server": { - "host": "", - "port": 1234, - "protocol": "https", - "client_id": "", - "access_token": "", - "verify_certificate": false, - "ca_filename": null - } -} \ No newline at end of file diff --git a/src/zepben/examples/sample_config.json b/src/zepben/examples/sample_config.json index 751bdec..d5ac15b 100644 --- a/src/zepben/examples/sample_config.json +++ b/src/zepben/examples/sample_config.json @@ -1,5 +1,7 @@ { - "host": "hostname", - "rpc_port": 1234, - "access_token": "your_access_token" -} \ No newline at end of file + "_comment": "Copy this file to config.json (git-ignored) in the same directory and fill in real values.", + "host": "", + "rpc_port": 443, + "ca_filename": null, + "access_token": "" +} diff --git a/src/zepben/examples/tx_id_to_name.py b/src/zepben/examples/tx_id_to_name.py index 05ca357..c370cfb 100644 --- a/src/zepben/examples/tx_id_to_name.py +++ b/src/zepben/examples/tx_id_to_name.py @@ -18,12 +18,12 @@ OUTPUT_FILE = "transformer_id_mapping.csv" HEADER = True -with open("./config.json") as f: +with open("config.json") as f: c = json.loads(f.read()) async def connect(): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c["ca_path"]) + channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) network_client = NetworkConsumerClient(channel=channel) if os.path.exists(OUTPUT_FILE): diff --git a/src/zepben/examples/utils.py b/src/zepben/examples/utils.py index d5e0e5e..3d857c9 100644 --- a/src/zepben/examples/utils.py +++ b/src/zepben/examples/utils.py @@ -13,7 +13,7 @@ logger = logging.getLogger() -# Default config dir is where the sample_auth_config sits. +# Default config dir is where config.json sits. def get_config_dir(argv): return argv[1] if len(argv) > 1 else "." @@ -26,15 +26,15 @@ def read_json_config(config_file_path: str) -> Dict: def get_client(config_dir, async_=True): - # Change sample_auth_config.json to any other file name - auth_config = read_json_config(f"{config_dir}/sample_auth_config.json") + # Change config.json to any other file name + config = read_json_config(f"{config_dir}/config.json") return EasClient( - host=auth_config["eas_server"]["host"], - port=auth_config["eas_server"]["port"], - protocol=auth_config["eas_server"]["protocol"], - access_token=auth_config["eas_server"]["access_token"], - verify_certificate=auth_config["eas_server"].get("verify_certificate", True), - ca_filename=auth_config["eas_server"].get("ca_filename"), + host=config["host"], + port=config["rpc_port"], + protocol="https", + access_token=config["access_token"], + verify_certificate=config.get("verify_certificate", True), + ca_filename=config.get("ca_filename"), asynchronous=async_ ) From 11ff031aec6b6af52fb3fd0ad570343aa85c0e3d Mon Sep 17 00:00:00 2001 From: Alex Vollebergh Date: Tue, 7 Jul 2026 12:07:16 +1000 Subject: [PATCH 2/5] update configs to **c Signed-off-by: Alex Vollebergh --- src/zepben/examples/cim/extract_hv_customers.py | 2 +- src/zepben/examples/cim/extract_lv_substations.py | 2 +- .../examples/cim/extract_per_phase_wire_info.py | 2 +- src/zepben/examples/id_csv_generator.py | 2 +- .../examples/request_feeder_load_analysis_study.py | 11 ++--------- src/zepben/examples/request_network_model_ingest.py | 11 ++--------- src/zepben/examples/tx_id_to_name.py | 2 +- src/zepben/examples/utils.py | 12 ++---------- 8 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/zepben/examples/cim/extract_hv_customers.py b/src/zepben/examples/cim/extract_hv_customers.py index 3ffcf74..7e42b44 100644 --- a/src/zepben/examples/cim/extract_hv_customers.py +++ b/src/zepben/examples/cim/extract_hv_customers.py @@ -15,7 +15,7 @@ async def extract_hv_customers_for_feeder(feeder_mrid: str): - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c.get("ca_filename")) + channel = connect_with_token(**c) network_client = NetworkConsumerClient(channel=channel) network = network_client.service diff --git a/src/zepben/examples/cim/extract_lv_substations.py b/src/zepben/examples/cim/extract_lv_substations.py index 514dee2..1cbd298 100644 --- a/src/zepben/examples/cim/extract_lv_substations.py +++ b/src/zepben/examples/cim/extract_lv_substations.py @@ -15,7 +15,7 @@ async def extract_lv_substations_for_feeder(feeder_mrid: str): - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"], ca_filename=c.get("ca_filename")) + channel = connect_with_token(**c) network_client = NetworkConsumerClient(channel=channel) network = network_client.service diff --git a/src/zepben/examples/cim/extract_per_phase_wire_info.py b/src/zepben/examples/cim/extract_per_phase_wire_info.py index 80b4933..4644a94 100644 --- a/src/zepben/examples/cim/extract_per_phase_wire_info.py +++ b/src/zepben/examples/cim/extract_per_phase_wire_info.py @@ -15,7 +15,7 @@ async def extract_wire_info_per_phase(feeder_mrid: str): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) + channel = connect_with_token(**c) network_client = NetworkConsumerClient(channel=channel) network = network_client.service (await network_client.get_equipment_container(feeder_mrid, diff --git a/src/zepben/examples/id_csv_generator.py b/src/zepben/examples/id_csv_generator.py index 65d4497..a979307 100644 --- a/src/zepben/examples/id_csv_generator.py +++ b/src/zepben/examples/id_csv_generator.py @@ -26,7 +26,7 @@ async def connect(): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) + channel = connect_with_token(**c) network_client = NetworkConsumerClient(channel=channel) network_hierarchy = (await network_client.get_network_hierarchy()).throw_on_error().value diff --git a/src/zepben/examples/request_feeder_load_analysis_study.py b/src/zepben/examples/request_feeder_load_analysis_study.py index 575ea84..0f29510 100644 --- a/src/zepben/examples/request_feeder_load_analysis_study.py +++ b/src/zepben/examples/request_feeder_load_analysis_study.py @@ -16,15 +16,8 @@ async def main(argv): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EAS..") - eas_client = EasClient( - host=c["host"], - port=c["rpc_port"], - protocol="https", - access_token=c["access_token"], - verify_certificate=c.get("verify_certificate", True), - ca_filename=c.get("ca_filename"), - asynchronous=True - ) + rpc_port = c.pop("rpc_port") + eas_client = EasClient(**c, port=rpc_port, protocol="https", asynchronous=True) print("Connection established..") # Fire off a feeder load analysis study feeder_load_analysis_token = await eas_client.mutation( diff --git a/src/zepben/examples/request_network_model_ingest.py b/src/zepben/examples/request_network_model_ingest.py index 650c01a..b89f976 100644 --- a/src/zepben/examples/request_network_model_ingest.py +++ b/src/zepben/examples/request_network_model_ingest.py @@ -19,15 +19,8 @@ async def main(argv): print("Connecting to EAS..") - eas_client = EasClient( - host=c["host"], - port=c["rpc_port"], - protocol="https", - access_token=c["access_token"], - verify_certificate=c.get("verify_certificate", True), - ca_filename=c.get("ca_filename"), - asynchronous=True - ) + rpc_port = c.pop("rpc_port") + eas_client = EasClient(**c, port=rpc_port, protocol="https", asynchronous=True) print("Connection established..") # Kick off a network model ingest. # diff --git a/src/zepben/examples/tx_id_to_name.py b/src/zepben/examples/tx_id_to_name.py index c370cfb..ca1dfdd 100644 --- a/src/zepben/examples/tx_id_to_name.py +++ b/src/zepben/examples/tx_id_to_name.py @@ -23,7 +23,7 @@ async def connect(): - channel = connect_with_token(host=c["host"], rpc_port=c["rpc_port"], access_token=c["access_token"], ca_filename=c.get("ca_filename")) + channel = connect_with_token(**c) network_client = NetworkConsumerClient(channel=channel) if os.path.exists(OUTPUT_FILE): diff --git a/src/zepben/examples/utils.py b/src/zepben/examples/utils.py index 3d857c9..c3713a1 100644 --- a/src/zepben/examples/utils.py +++ b/src/zepben/examples/utils.py @@ -26,15 +26,7 @@ def read_json_config(config_file_path: str) -> Dict: def get_client(config_dir, async_=True): - # Change config.json to any other file name config = read_json_config(f"{config_dir}/config.json") + rpc_port = config.pop("rpc_port") - return EasClient( - host=config["host"], - port=config["rpc_port"], - protocol="https", - access_token=config["access_token"], - verify_certificate=config.get("verify_certificate", True), - ca_filename=config.get("ca_filename"), - asynchronous=async_ - ) + return EasClient(**config, port=rpc_port, protocol="https", asynchronous=async_) From d6a2067f7a0e1a579035068a7f31bdb55807c805 Mon Sep 17 00:00:00 2001 From: Alex Vollebergh Date: Tue, 7 Jul 2026 17:40:22 +1000 Subject: [PATCH 3/5] Split example config into ewb and eas sections --- src/zepben/examples/all_ratings_csv.py | 5 ++--- .../examples/cim/extract_hv_customers.py | 2 +- .../examples/cim/extract_lv_substations.py | 2 +- .../cim/extract_per_phase_wire_info.py | 2 +- .../examples/connecting_to_grpc_service.py | 8 ++------ .../examples/current_state_manipulations.py | 2 +- src/zepben/examples/dsub_from_nmi.py | 4 ++-- .../examples/element_network_hierarchy.py | 2 +- .../energy_consumer_device_hierarchy.py | 3 +-- src/zepben/examples/export_open_dss_model.py | 9 ++------- .../examples/fetching_network_hierarchy.py | 4 ++-- src/zepben/examples/fetching_network_model.py | 4 ++-- .../find_isolation_section_from_equipment.py | 8 ++------ src/zepben/examples/id_csv_generator.py | 2 +- .../isolation_equipment_between_nodes.py | 8 ++------ .../request_feeder_load_analysis_study.py | 5 ++--- .../examples/request_network_model_ingest.py | 5 ++--- src/zepben/examples/sample_config.json | 19 ++++++++++++++----- .../studies/creating_and_uploading_study.py | 18 +++++------------- .../examples/studies/suspect_end_of_line.py | 10 ++++++---- src/zepben/examples/test.ipynb | 6 +++--- .../tracing_conductor_type_by_lv_circuit.py | 4 ++-- src/zepben/examples/tracing_example.py | 4 ++-- src/zepben/examples/tx_id_to_name.py | 2 +- src/zepben/examples/utils.py | 5 ++--- 25 files changed, 62 insertions(+), 81 deletions(-) diff --git a/src/zepben/examples/all_ratings_csv.py b/src/zepben/examples/all_ratings_csv.py index 6bee9fa..fc37759 100644 --- a/src/zepben/examples/all_ratings_csv.py +++ b/src/zepben/examples/all_ratings_csv.py @@ -30,10 +30,9 @@ class EquipmentWithRating: def _get_client(): with open('config.json') as f: - c = json.load(f) + c = json.load(f)['ewb'] - # Connect to server - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token(**c) return NetworkConsumerClient(channel) diff --git a/src/zepben/examples/cim/extract_hv_customers.py b/src/zepben/examples/cim/extract_hv_customers.py index 7e42b44..13e6ccb 100644 --- a/src/zepben/examples/cim/extract_hv_customers.py +++ b/src/zepben/examples/cim/extract_hv_customers.py @@ -11,7 +11,7 @@ from zepben.examples import CONFIG_DIR with open(f"{CONFIG_DIR}/config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] async def extract_hv_customers_for_feeder(feeder_mrid: str): diff --git a/src/zepben/examples/cim/extract_lv_substations.py b/src/zepben/examples/cim/extract_lv_substations.py index 1cbd298..2f2b6e9 100644 --- a/src/zepben/examples/cim/extract_lv_substations.py +++ b/src/zepben/examples/cim/extract_lv_substations.py @@ -11,7 +11,7 @@ from zepben.examples import CONFIG_DIR with open(f"{CONFIG_DIR}/config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] async def extract_lv_substations_for_feeder(feeder_mrid: str): diff --git a/src/zepben/examples/cim/extract_per_phase_wire_info.py b/src/zepben/examples/cim/extract_per_phase_wire_info.py index 4644a94..45cb420 100644 --- a/src/zepben/examples/cim/extract_per_phase_wire_info.py +++ b/src/zepben/examples/cim/extract_per_phase_wire_info.py @@ -11,7 +11,7 @@ from zepben.examples import CONFIG_DIR with open(f"{CONFIG_DIR}/config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] async def extract_wire_info_per_phase(feeder_mrid: str): diff --git a/src/zepben/examples/connecting_to_grpc_service.py b/src/zepben/examples/connecting_to_grpc_service.py index 3122df8..be4c357 100644 --- a/src/zepben/examples/connecting_to_grpc_service.py +++ b/src/zepben/examples/connecting_to_grpc_service.py @@ -114,14 +114,10 @@ async def connect_using_token(): from zepben.ewb import connect_with_token, NetworkConsumerClient with open("config.json") as f: - c = json.load(f) + c = json.load(f)["ewb"] print("Connecting to EWB..") - channel = connect_with_token( - host=c["host"], - access_token=c["access_token"], - rpc_port=c["rpc_port"] - ) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) print("Connection established..") print("Printing network hierarchy..") diff --git a/src/zepben/examples/current_state_manipulations.py b/src/zepben/examples/current_state_manipulations.py index 4cd3cc1..b12d973 100644 --- a/src/zepben/examples/current_state_manipulations.py +++ b/src/zepben/examples/current_state_manipulations.py @@ -246,7 +246,7 @@ async def main(): # noinspection PyTypeChecker with open('config.json') as f: - config = json.load(f) + config = json.load(f)['ewb'] async with connect_with_token(**config) as secure_channel: await run_simple(NetworkConsumerClient(secure_channel)) await run_swap_feeder(NetworkConsumerClient(secure_channel)) diff --git a/src/zepben/examples/dsub_from_nmi.py b/src/zepben/examples/dsub_from_nmi.py index c7e5c0b..83a5ff3 100644 --- a/src/zepben/examples/dsub_from_nmi.py +++ b/src/zepben/examples/dsub_from_nmi.py @@ -20,7 +20,7 @@ with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] def _trace(start_item, results, stop_condition): @@ -44,7 +44,7 @@ def step_action(step: NetworkTraceStep, context: StepContext): async def main(mrid: str, feeder_mrid: str): - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) await client.get_equipment_container(feeder_mrid, include_energized_containers=IncludedEnergizedContainers.LV_FEEDERS) network = client.service diff --git a/src/zepben/examples/element_network_hierarchy.py b/src/zepben/examples/element_network_hierarchy.py index 651f3d4..34ab139 100644 --- a/src/zepben/examples/element_network_hierarchy.py +++ b/src/zepben/examples/element_network_hierarchy.py @@ -25,7 +25,7 @@ def _get_client() -> NetworkConsumerClient: with open('config.json') as f: - config = json.load(f) + config = json.load(f)['ewb'] return NetworkConsumerClient(connect_with_token(**config)) diff --git a/src/zepben/examples/energy_consumer_device_hierarchy.py b/src/zepben/examples/energy_consumer_device_hierarchy.py index d39dfe0..02b60af 100644 --- a/src/zepben/examples/energy_consumer_device_hierarchy.py +++ b/src/zepben/examples/energy_consumer_device_hierarchy.py @@ -32,9 +32,8 @@ class EnergyConsumerDeviceHierarchy: def _get_client(): with open('config.json') as f: - config = json.load(f) + config = json.load(f)['ewb'] - # Connect to server channel = connect_with_token(**config) return NetworkConsumerClient(channel) diff --git a/src/zepben/examples/export_open_dss_model.py b/src/zepben/examples/export_open_dss_model.py index 80ddd8e..2cde810 100644 --- a/src/zepben/examples/export_open_dss_model.py +++ b/src/zepben/examples/export_open_dss_model.py @@ -15,7 +15,7 @@ with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["eas"] def wait_for_export(eas_client: EasClient, model_id: int): @@ -66,12 +66,7 @@ def download_generated_model(eas_client: EasClient, output_file_name: str, model def open_dss_export(export_file_name: str): - eas_client = EasClient( - host=c["host"], - port=c["rpc_port"], - access_token=c["access_token"], - asynchronous=True - ) + eas_client = EasClient(**c, asynchronous=True) # Run an opendss export print("Sending OpenDss model export request to EAS") diff --git a/src/zepben/examples/fetching_network_hierarchy.py b/src/zepben/examples/fetching_network_hierarchy.py index 9e504ca..19fe3bb 100644 --- a/src/zepben/examples/fetching_network_hierarchy.py +++ b/src/zepben/examples/fetching_network_hierarchy.py @@ -9,13 +9,13 @@ from zepben.ewb import connect_with_token, NetworkConsumerClient with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] async def main(): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EWB..") - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) print("Connection established..") # Fetch network hierarchy diff --git a/src/zepben/examples/fetching_network_model.py b/src/zepben/examples/fetching_network_model.py index 5196926..b798013 100644 --- a/src/zepben/examples/fetching_network_model.py +++ b/src/zepben/examples/fetching_network_model.py @@ -17,8 +17,8 @@ async def main(): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EWB..") with open("config.json") as f: - c = json.loads(f.read()) - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + c = json.loads(f.read())["ewb"] + channel = connect_with_token(**c) feeder_mrid = "WD24" print(f"Fetching {feeder_mrid}") # Note you should create a new client for each Feeder you retrieve diff --git a/src/zepben/examples/find_isolation_section_from_equipment.py b/src/zepben/examples/find_isolation_section_from_equipment.py index 784dc1e..030aa2c 100644 --- a/src/zepben/examples/find_isolation_section_from_equipment.py +++ b/src/zepben/examples/find_isolation_section_from_equipment.py @@ -21,13 +21,9 @@ async def main(conductor_mrid: str, feeder_mrid: str): with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] - channel = connect_with_token( - host=c["host"], - access_token=c["access_token"], - rpc_port=c["rpc_port"] - ) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) await client.get_equipment_container( feeder_mrid, include_energized_containers=IncludedEnergizedContainers.LV_FEEDERS diff --git a/src/zepben/examples/id_csv_generator.py b/src/zepben/examples/id_csv_generator.py index a979307..da2aeb5 100644 --- a/src/zepben/examples/id_csv_generator.py +++ b/src/zepben/examples/id_csv_generator.py @@ -17,7 +17,7 @@ from zepben.ewb import NetworkConsumerClient, connect_with_token, ConductingEquipment, Feeder, IncludedEnergizedContainers with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] """ This is a basic example that shows how to export a CSV of all the conducting equipment in a feeder. diff --git a/src/zepben/examples/isolation_equipment_between_nodes.py b/src/zepben/examples/isolation_equipment_between_nodes.py index 5b2be0c..864097e 100644 --- a/src/zepben/examples/isolation_equipment_between_nodes.py +++ b/src/zepben/examples/isolation_equipment_between_nodes.py @@ -22,13 +22,9 @@ async def main(mrids: Tuple[str, str], io_type: Type[ProtectedSwitch], feeder_mrid): with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] - channel = connect_with_token( - host=c["host"], - access_token=c["access_token"], - rpc_port=c["rpc_port"] - ) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) await client.get_equipment_container( feeder_mrid, diff --git a/src/zepben/examples/request_feeder_load_analysis_study.py b/src/zepben/examples/request_feeder_load_analysis_study.py index 0f29510..cc33f77 100644 --- a/src/zepben/examples/request_feeder_load_analysis_study.py +++ b/src/zepben/examples/request_feeder_load_analysis_study.py @@ -10,14 +10,13 @@ from zepben.eas import FeederLoadAnalysisInput, EasClient, Mutation with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["eas"] async def main(argv): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EAS..") - rpc_port = c.pop("rpc_port") - eas_client = EasClient(**c, port=rpc_port, protocol="https", asynchronous=True) + eas_client = EasClient(**c, asynchronous=True) print("Connection established..") # Fire off a feeder load analysis study feeder_load_analysis_token = await eas_client.mutation( diff --git a/src/zepben/examples/request_network_model_ingest.py b/src/zepben/examples/request_network_model_ingest.py index b89f976..e429623 100644 --- a/src/zepben/examples/request_network_model_ingest.py +++ b/src/zepben/examples/request_network_model_ingest.py @@ -14,13 +14,12 @@ # files in blob storage before running this script. with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["eas"] async def main(argv): print("Connecting to EAS..") - rpc_port = c.pop("rpc_port") - eas_client = EasClient(**c, port=rpc_port, protocol="https", asynchronous=True) + eas_client = EasClient(**c, asynchronous=True) print("Connection established..") # Kick off a network model ingest. # diff --git a/src/zepben/examples/sample_config.json b/src/zepben/examples/sample_config.json index d5ac15b..4950ed7 100644 --- a/src/zepben/examples/sample_config.json +++ b/src/zepben/examples/sample_config.json @@ -1,7 +1,16 @@ { - "_comment": "Copy this file to config.json (git-ignored) in the same directory and fill in real values.", - "host": "", - "rpc_port": 443, - "ca_filename": null, - "access_token": "" + "_comment": "Copy this file to config.json (git-ignored) in the same directory and fill in real values. 'ewb' is used by EWB/NetworkConsumerClient examples (connect_with_token); 'eas' is used by EAS/EasClient examples. Each section's keys are passed straight through as **kwargs to that client, so the keys must match its constructor args.", + "ewb": { + "host": "", + "rpc_port": 443, + "ca_filename": null, + "access_token": "" + }, + "eas": { + "host": "", + "port": 443, + "protocol": "https", + "ca_filename": null, + "access_token": "" + } } diff --git a/src/zepben/examples/studies/creating_and_uploading_study.py b/src/zepben/examples/studies/creating_and_uploading_study.py index e8ec479..31d194d 100644 --- a/src/zepben/examples/studies/creating_and_uploading_study.py +++ b/src/zepben/examples/studies/creating_and_uploading_study.py @@ -18,17 +18,15 @@ with open("../config.json") as f: - c = json.loads(f.read()) + _config = json.loads(f.read()) + c_ewb = _config["ewb"] + c_eas = _config["eas"] async def main(): # Fetch network model from Energy Workbench's gRPC service (see ../connecting_to_grpc_service.py for examples on different connection functions) print("Connecting to EWB..") - grpc_channel = connect_with_token( - host=c["host"], - access_token=c["access_token"], - rpc_port=c["rpc_port"] - ) + grpc_channel = connect_with_token(**c_ewb) feeder_mrid = "WD24" grpc_client = NetworkConsumerClient(grpc_channel) @@ -91,13 +89,7 @@ async def main(): ) print("Study created..") print("Connecting to EAS..") - eas_client = EasClient( - host=c["host"], - port=c["rpc_port"], - protocol="https", - access_token=c["access_token"], - asynchronous=True - ) + eas_client = EasClient(**c_eas, asynchronous=True) print("Connection established..") diff --git a/src/zepben/examples/studies/suspect_end_of_line.py b/src/zepben/examples/studies/suspect_end_of_line.py index 85189a6..0bbb278 100644 --- a/src/zepben/examples/studies/suspect_end_of_line.py +++ b/src/zepben/examples/studies/suspect_end_of_line.py @@ -19,7 +19,9 @@ with open("../config.json") as f: - c = json.loads(f.read()) + _config = json.loads(f.read()) + c_ewb = _config["ewb"] + c_eas = _config["eas"] def chunk(it, size): @@ -32,7 +34,7 @@ async def main(): zone_mrids = ["MTN"] print(f"Start time: {datetime.now()}") - rpc_channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + rpc_channel = connect_with_token(**c_ewb) client = NetworkConsumerClient(rpc_channel) hierarchy = (await client.get_network_hierarchy()).throw_on_error() substations = hierarchy.value.substations @@ -54,7 +56,7 @@ async def main(): all_traced_equipment = [] transformer_to_suspect_end = dict() - rpc_channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + rpc_channel = connect_with_token(**c_ewb) print(f"Processing feeders {', '.join(feeders)}") for feeder_mrid in feeders: futures.append(asyncio.ensure_future(fetch_feeder_and_trace(feeder_mrid, rpc_channel))) @@ -67,7 +69,7 @@ async def main(): print(f"Created Study for {len(feeder_mrids)} feeders") - eas_client = EasClient(host=c["host"], port=c["rpc_port"], protocol="https", access_token=c["access_token"], asynchronous=True) + eas_client = EasClient(**c_eas, asynchronous=True) print(f"Uploading Study for {', '.join(feeders)} ...") await upload_suspect_end_of_line_study( diff --git a/src/zepben/examples/test.ipynb b/src/zepben/examples/test.ipynb index 4976b42..b9713ed 100644 --- a/src/zepben/examples/test.ipynb +++ b/src/zepben/examples/test.ipynb @@ -16,13 +16,13 @@ "from zepben.evolve import connect_with_token, NetworkConsumerClient\n", "\n", "with open(\"config.json\") as f:\n", - " c = json.loads(f.read())\n", + " c = json.loads(f.read())[\"ewb\"]\n", "\n", "\n", "async def main():\n", " # See connecting_to_grpc_service.py for examples of each connect function\n", " print(\"Connecting to EWB..\")\n", - " channel = connect_with_token(host=c[\"host\"], access_token=c[\"access_token\"], rpc_port=c[\"rpc_port\"])\n", + " channel = connect_with_token(**c)\n", " client = NetworkConsumerClient(channel)\n", " print(\"Connection established..\")\n", " # Fetch network hierarchy\n", @@ -50,4 +50,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py index 247cd8b..4d5792c 100644 --- a/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py +++ b/src/zepben/examples/tracing_conductor_type_by_lv_circuit.py @@ -18,10 +18,10 @@ async def main(): with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] print("Connecting to Server") - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) result = (await client.get_network_hierarchy()).throw_on_error().result diff --git a/src/zepben/examples/tracing_example.py b/src/zepben/examples/tracing_example.py index 6bb85b7..8a71cd0 100644 --- a/src/zepben/examples/tracing_example.py +++ b/src/zepben/examples/tracing_example.py @@ -17,10 +17,10 @@ async def main(): with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] print("Connecting to Server") - channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) + channel = connect_with_token(**c) client = NetworkConsumerClient(channel) result = (await client.get_network_hierarchy()).throw_on_error().result diff --git a/src/zepben/examples/tx_id_to_name.py b/src/zepben/examples/tx_id_to_name.py index ca1dfdd..40dc824 100644 --- a/src/zepben/examples/tx_id_to_name.py +++ b/src/zepben/examples/tx_id_to_name.py @@ -19,7 +19,7 @@ HEADER = True with open("config.json") as f: - c = json.loads(f.read()) + c = json.loads(f.read())["ewb"] async def connect(): diff --git a/src/zepben/examples/utils.py b/src/zepben/examples/utils.py index c3713a1..2545bc7 100644 --- a/src/zepben/examples/utils.py +++ b/src/zepben/examples/utils.py @@ -26,7 +26,6 @@ def read_json_config(config_file_path: str) -> Dict: def get_client(config_dir, async_=True): - config = read_json_config(f"{config_dir}/config.json") - rpc_port = config.pop("rpc_port") + config = read_json_config(f"{config_dir}/config.json")["eas"] - return EasClient(**config, port=rpc_port, protocol="https", asynchronous=async_) + return EasClient(**config, asynchronous=async_) From ce0f4c91a997c2faf506aa0802d6fb45625e4d86 Mon Sep 17 00:00:00 2001 From: Alex Vollebergh Date: Wed, 8 Jul 2026 10:03:45 +1000 Subject: [PATCH 4/5] updated async handling Signed-off-by: Alex Vollebergh --- src/zepben/examples/export_open_dss_model.py | 4 +++- .../examples/request_feeder_load_analysis_study.py | 6 ++++-- src/zepben/examples/request_network_model_ingest.py | 6 ++++-- src/zepben/examples/sample_config.json | 2 +- .../studies/creating_and_uploading_study.py | 6 ++++-- src/zepben/examples/studies/suspect_end_of_line.py | 4 +++- src/zepben/examples/utils.py | 13 ++++++++++++- 7 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/zepben/examples/export_open_dss_model.py b/src/zepben/examples/export_open_dss_model.py index 2cde810..bcf4f6f 100644 --- a/src/zepben/examples/export_open_dss_model.py +++ b/src/zepben/examples/export_open_dss_model.py @@ -13,6 +13,8 @@ from time import sleep import requests +from zepben.examples.utils import eas_client_from_config + with open("config.json") as f: c = json.loads(f.read())["eas"] @@ -66,7 +68,7 @@ def download_generated_model(eas_client: EasClient, output_file_name: str, model def open_dss_export(export_file_name: str): - eas_client = EasClient(**c, asynchronous=True) + eas_client = eas_client_from_config(c, asynchronous=True) # Run an opendss export print("Sending OpenDss model export request to EAS") diff --git a/src/zepben/examples/request_feeder_load_analysis_study.py b/src/zepben/examples/request_feeder_load_analysis_study.py index cc33f77..42ecbe8 100644 --- a/src/zepben/examples/request_feeder_load_analysis_study.py +++ b/src/zepben/examples/request_feeder_load_analysis_study.py @@ -7,7 +7,9 @@ import json import sys -from zepben.eas import FeederLoadAnalysisInput, EasClient, Mutation +from zepben.eas import FeederLoadAnalysisInput, Mutation + +from zepben.examples.utils import eas_client_from_config with open("config.json") as f: c = json.loads(f.read())["eas"] @@ -16,7 +18,7 @@ async def main(argv): # See connecting_to_grpc_service.py for examples of each connect function print("Connecting to EAS..") - eas_client = EasClient(**c, asynchronous=True) + eas_client = eas_client_from_config(c, asynchronous=True) print("Connection established..") # Fire off a feeder load analysis study feeder_load_analysis_token = await eas_client.mutation( diff --git a/src/zepben/examples/request_network_model_ingest.py b/src/zepben/examples/request_network_model_ingest.py index e429623..51d5579 100644 --- a/src/zepben/examples/request_network_model_ingest.py +++ b/src/zepben/examples/request_network_model_ingest.py @@ -7,7 +7,9 @@ import json import sys -from zepben.eas import FeederLoadAnalysisInput, EasClient, Mutation, IngestorConfigInput +from zepben.eas import FeederLoadAnalysisInput, Mutation, IngestorConfigInput + +from zepben.examples.utils import eas_client_from_config # This example kicks off an ingest of a network model that has already been uploaded to blob storage. # See docs/docs/network_model_ingest.mdx for a full walkthrough, including how to prepare the model @@ -19,7 +21,7 @@ async def main(argv): print("Connecting to EAS..") - eas_client = EasClient(**c, asynchronous=True) + eas_client = eas_client_from_config(c, asynchronous=True) print("Connection established..") # Kick off a network model ingest. # diff --git a/src/zepben/examples/sample_config.json b/src/zepben/examples/sample_config.json index 4950ed7..f925a00 100644 --- a/src/zepben/examples/sample_config.json +++ b/src/zepben/examples/sample_config.json @@ -1,5 +1,5 @@ { - "_comment": "Copy this file to config.json (git-ignored) in the same directory and fill in real values. 'ewb' is used by EWB/NetworkConsumerClient examples (connect_with_token); 'eas' is used by EAS/EasClient examples. Each section's keys are passed straight through as **kwargs to that client, so the keys must match its constructor args.", + "_comment": "Copy this file to config.json (git-ignored) in the same directory and fill in real values. 'ewb' is used by EWB/NetworkConsumerClient examples (connect_with_token); 'eas' is used by EAS/EasClient examples. Each section's keys are passed straight through as **kwargs to that client, so the keys must match its constructor args. Do not add 'asynchronous' here - it's a per-script call choice, not a connection setting, and most scripts hardcode it anyway.", "ewb": { "host": "", "rpc_port": 443, diff --git a/src/zepben/examples/studies/creating_and_uploading_study.py b/src/zepben/examples/studies/creating_and_uploading_study.py index 31d194d..c0da45b 100644 --- a/src/zepben/examples/studies/creating_and_uploading_study.py +++ b/src/zepben/examples/studies/creating_and_uploading_study.py @@ -7,8 +7,10 @@ import json from geojson import Feature, LineString, FeatureCollection, Point -from zepben.eas import StudyInput, StudyResultInput, GeoJsonOverlayInput, EasClient, Mutation +from zepben.eas import StudyInput, StudyResultInput, GeoJsonOverlayInput, Mutation from zepben.ewb import AcLineSegment, EnergyConsumer, connect_with_token, NetworkConsumerClient, IncludedEnergizedContainers + +from zepben.examples.utils import eas_client_from_config # A study is a geographical visualisation of data that is drawn on top of the network. # This data is typically the result of a load flow simulation. # Each study may contain multiple results: different visualisations that the user may switch between. @@ -89,7 +91,7 @@ async def main(): ) print("Study created..") print("Connecting to EAS..") - eas_client = EasClient(**c_eas, asynchronous=True) + eas_client = eas_client_from_config(c_eas, asynchronous=True) print("Connection established..") diff --git a/src/zepben/examples/studies/suspect_end_of_line.py b/src/zepben/examples/studies/suspect_end_of_line.py index 0bbb278..a805c56 100644 --- a/src/zepben/examples/studies/suspect_end_of_line.py +++ b/src/zepben/examples/studies/suspect_end_of_line.py @@ -17,6 +17,8 @@ NetworkConsumerClient, PhaseCode, PowerElectronicsConnection, Feeder, PowerSystemResource, Location, \ connect_with_token, NetworkTraceStep, Tracing, downstream, upstream, IncludedEnergizedContainers +from zepben.examples.utils import eas_client_from_config + with open("../config.json") as f: _config = json.loads(f.read()) @@ -69,7 +71,7 @@ async def main(): print(f"Created Study for {len(feeder_mrids)} feeders") - eas_client = EasClient(**c_eas, asynchronous=True) + eas_client = eas_client_from_config(c_eas, asynchronous=True) print(f"Uploading Study for {', '.join(feeders)} ...") await upload_suspect_end_of_line_study( diff --git a/src/zepben/examples/utils.py b/src/zepben/examples/utils.py index 2545bc7..f8bcfc6 100644 --- a/src/zepben/examples/utils.py +++ b/src/zepben/examples/utils.py @@ -25,7 +25,18 @@ def read_json_config(config_file_path: str) -> Dict: return config_dict +def eas_client_from_config(config: Dict, asynchronous: bool) -> EasClient: + """ + Builds an EasClient from an 'eas' config section. asynchronous is always a call-site + choice (the caller always wants True or always wants False), so it overrides whatever + an 'asynchronous' key in config might say, rather than colliding with it as a duplicate + kwarg. + """ + config = {k: v for k, v in config.items() if k != "asynchronous"} + return EasClient(**config, asynchronous=asynchronous) + + def get_client(config_dir, async_=True): config = read_json_config(f"{config_dir}/config.json")["eas"] - return EasClient(**config, asynchronous=async_) + return eas_client_from_config(config, async_) From 7ce1ebf15651a80d2a076ca160bf9b1a660dfeaa Mon Sep 17 00:00:00 2001 From: Alex Vollebergh Date: Thu, 16 Jul 2026 13:16:50 +1000 Subject: [PATCH 5/5] updated some docs and tidied up Signed-off-by: Alex Vollebergh --- docs/docs/creating_and_uploading_studies.mdx | 53 ++++++++++------ docs/docs/network_model_ingest.mdx | 2 +- src/zepben/examples/list_feeder_meter_ids.py | 66 ++++++++++++++++++++ src/zepben/examples/utils.py | 10 +-- 4 files changed, 104 insertions(+), 27 deletions(-) create mode 100644 src/zepben/examples/list_feeder_meter_ids.py diff --git a/docs/docs/creating_and_uploading_studies.mdx b/docs/docs/creating_and_uploading_studies.mdx index 2cd4485..97f1976 100644 --- a/docs/docs/creating_and_uploading_studies.mdx +++ b/docs/docs/creating_and_uploading_studies.mdx @@ -17,16 +17,17 @@ Hence, you should ensure that both the servers are running for this example. Gui ## Getting Started -Ensure that you have installed the required dependencies and imported the libraries. For this example, we will use `json`, `geojson`, `zepben.eas` and `zepben.evolve` libraries. +Ensure that you have installed the required dependencies and imported the libraries. For this example, we will use `json`, `geojson`, `zepben.eas` and `zepben.ewb` libraries. Information about connecting to the Energy Workbench Server can be found in the [Connecting to the EWB Server](connecting-to-ewb.mdx) tutorial. ```python import json from geojson import Feature, LineString, FeatureCollection, Point -from zepben.eas import Study, Result, GeoJsonOverlay, EasClient -from zepben.evolve import connect_with_token, NetworkConsumerClient, AcLineSegment, EnergyConsumer -from zepben.protobuf.nc.nc_requests_pb2 import INCLUDE_ENERGIZED_LV_FEEDERS +from zepben.eas import StudyInput, StudyResultInput, GeoJsonOverlayInput, Mutation +from zepben.ewb import AcLineSegment, EnergyConsumer, connect_with_token, NetworkConsumerClient, IncludedEnergizedContainers + +from zepben.examples.utils import eas_client_from_config ``` ## Connecting to the EWB gRPC Service @@ -35,18 +36,30 @@ Here, we connect to the EWB Server, using the `connect_with_token` function. ```python with open("../config.json") as f: - c = json.loads(f.read()) + _config = json.loads(f.read()) + c_ewb = _config["ewb"] + c_eas = _config["eas"] -channel = connect_with_token(host=c["host"], access_token=c["access_token"], rpc_port=c["rpc_port"]) +channel = connect_with_token(**c_ewb) ``` -You will need to replace the values in the config.json file with the appropriate configuration for your environment. The config file should look like this: +You will need to replace the values in the config.json file with the appropriate configuration for your environment. The config file has separate `ewb` and `eas` sections - each section's keys are passed straight through as kwargs to the matching client, so they must match its constructor args. It should look like this: ```json { - "host": "EWB Hostname", - "access_token": "your_access_token", - "rpc_port": 1234 + "ewb": { + "host": "EWB Hostname", + "rpc_port": 1234, + "ca_filename": null, + "access_token": "your_access_token" + }, + "eas": { + "host": "EAS Hostname", + "port": 1234, + "protocol": "https", + "ca_filename": null, + "access_token": "your_access_token" + } } ``` More information about connecting to the EWB Server using different methods can be found in the [Connecting to the EWB Server](connecting-to-ewb.mdx) tutorial. @@ -54,13 +67,13 @@ More information about connecting to the EWB Server using different methods can You will need to specify the mRID of the feeder that you want to fetch, ensuring that the feeder with the specified mRID exists. Then, create a `NetworkConsumerClient` and retrieve the network service. The `get_equipment_container` method is used to fetch information about the specified feeder (feeder_mrid). -The `INCLUDE_ENERGIZED_LV_FEEDERS` flag indicates that the client should include information about energized LV (Low Voltage) feeders in the response. +The `IncludedEnergizedContainers.LV_FEEDERS` flag indicates that the client should include information about energized LV (Low Voltage) feeders in the response. ```python feeder_mrid = "LV007" grpc_client = NetworkConsumerClient(grpc_channel) -await grpc_client.get_equipment_container(feeder_mrid, include_energized_containers=INCLUDE_ENERGIZED_LV_FEEDERS) +await grpc_client.get_equipment_container(feeder_mrid, include_energized_containers=IncludedEnergizedContainers.LV_FEEDERS) network = grpc_client.service ``` @@ -70,7 +83,7 @@ We use the `EasClient` to connect to the Evolve App Server. `EasClient` is a cla For the examples in this tutorial, we will use the following code to connect to the EAS. ```python -eas_client = EasClient(host=c["host"], port=c["rpc_port"], protocol="https", access_token=c["access_token"]) +eas_client = eas_client_from_config(c_eas, asynchronous=True) ``` - **host** - The domain of the Evolve App Server, e.g. "evolve.local" @@ -78,7 +91,7 @@ eas_client = EasClient(host=c["host"], port=c["rpc_port"], protocol="https", acc - **access_token** - The personal access token you generated from the UI. - **protocol** - The protocol to use for connecting to EAS. Either http or https. -Note: Ensure that the values for host, port, and token are correctly provided in the config.json file. +Note: Ensure that the values for host, port, and token are correctly provided in the `eas` section of the config.json file. ## Create Results for Energy Consumers Density Example @@ -107,7 +120,8 @@ ec_result = StudyResultInput( geoJsonOverlay=GeoJsonOverlayInput( data=FeatureCollection(ec_geojson), styles=["ec-heatmap"] # Select which Mapbox layers to show for this result - ) + ), + sections=[] ) ``` @@ -159,7 +173,8 @@ lv_lines_result = StudyResultInput( geoJsonOverlay=GeoJsonOverlayInput( data=FeatureCollection(lv_lines_geojson), styles=["lv-lines", "lv-lengths"] # Select which Mapbox layers to show for this result - ) + ), + sections=[] ) ``` @@ -228,7 +243,7 @@ The following styles are used for "lv_lines" and "lv_lengths". Note that you wil ## Create and Upload Studies Once you have created the Results for the studies, the next step is to create and upload the study on EAS. -A data class (`Study`) represents an Evolve App Server study. The user need to provide information such as name, descriptions, tags, results, and styles to successfully create a study. +A data class (`StudyInput`) represents an Evolve App Server study. The user need to provide information such as name, descriptions, tags, results, and styles to successfully create a study. ```python study = StudyInput( @@ -253,7 +268,9 @@ Note: Each layer may have an entry in the legend via the metadata["zb:legend"] f The final step after creating the study is to upload it on the EAS and close the EAS client, as follows. ```python -await eas_client.upload_study(study) +response = await eas_client.mutation(Mutation.add_studies([study])) +print(response) + await eas_client.close() ``` diff --git a/docs/docs/network_model_ingest.mdx b/docs/docs/network_model_ingest.mdx index 52e25be..ee59cd9 100644 --- a/docs/docs/network_model_ingest.mdx +++ b/docs/docs/network_model_ingest.mdx @@ -15,7 +15,7 @@ Folder naming and internal file formatting is customer-specific - Zepben will pr ## Part 2: Running the ingestion kick-off script -Once your model folder is in place, run the example script to trigger the ingest. It first connects to the Evolve App Server using an `EasClient`, built from the same `config.json` values (`eas_host`, `eas_port`, `eas_protocol`, `access_token`, etc.) used throughout the other EAS examples. See [Creating and Uploading Studies](creating_and_uploading_studies.mdx) for a breakdown of these connection values. +Once your model folder is in place, run the example script to trigger the ingest. It first connects to the Evolve App Server using an `EasClient`, built from the `eas` section of `config.json` (`host`, `port`, `protocol`, `access_token`, etc.) used throughout the other EAS examples. See [Creating and Uploading Studies](creating_and_uploading_studies.mdx) for a breakdown of these connection values. It then triggers the ingest via `Mutation.execute_ingestor`, passing an `IngestorConfigInput` with `key="dataStorePath"` and `value` set to your folder name from Part 1. `run_config` is a list, so more key/value pairs can be added if Zepben provides other ingestor configuration options for your environment. diff --git a/src/zepben/examples/list_feeder_meter_ids.py b/src/zepben/examples/list_feeder_meter_ids.py new file mode 100644 index 0000000..2ae42d5 --- /dev/null +++ b/src/zepben/examples/list_feeder_meter_ids.py @@ -0,0 +1,66 @@ +# 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 asyncio +import csv +import json +import os + +from zepben.ewb import connect_with_token, NetworkConsumerClient, Meter, IncludedEnergizedContainers + + +def _get_client() -> NetworkConsumerClient: + with open('config.json') as f: + config = json.load(f)['ewb'] + + channel = connect_with_token(**config) + return NetworkConsumerClient(channel) + + +async def get_feeder_meter_ids(feeder_name_or_mrid: str, client: NetworkConsumerClient = None) -> list[str]: + """Fetch all Meters under the feeder identified by name or mrid, return their `name` (company_meter_id).""" + client = client or _get_client() + + hierarchy_response = await client.get_network_hierarchy() + hierarchy_response.throw_on_error() + hierarchy = hierarchy_response.result + feeder = hierarchy.feeders.get(feeder_name_or_mrid) + if feeder is None: + feeder = next((f for f in hierarchy.feeders.values() if f.name == feeder_name_or_mrid), None) + if feeder is None: + raise ValueError(f"No feeder found with mrid/name '{feeder_name_or_mrid}'") + + (await client.get_equipment_container( + feeder.mrid, + include_energized_containers=IncludedEnergizedContainers.LV_FEEDERS + )).throw_on_error() + + def _meter_id(meter: Meter) -> str: + # `name` holds the meter id when populated; otherwise it's embedded in the mrid as "-mt". + if meter.name: + return meter.name + return meter.mrid[:-3] if meter.mrid.endswith("-mt") else meter.mrid + + meter_ids = sorted({_meter_id(meter) for meter in client.service.objects(Meter)}) + return meter_ids + + +def write_csv(meter_ids: list[str], feeder_name_or_mrid: str) -> str: + os.makedirs("csvs", exist_ok=True) + path = f"csvs/{feeder_name_or_mrid}_meter_ids.csv" + with open(path, "w", newline="") as f: + writer = csv.writer(f) + writer.writerow(["meter_id"]) + for meter_id in meter_ids: + writer.writerow([meter_id]) + return path + + +if __name__ == "__main__": + FEEDER_NAME_OR_MRID = "" + ids = asyncio.run(get_feeder_meter_ids(FEEDER_NAME_OR_MRID)) + csv_path = write_csv(ids, FEEDER_NAME_OR_MRID) + print(f"Found {len(ids)} meter ids for feeder {FEEDER_NAME_OR_MRID}. Written to {csv_path}") diff --git a/src/zepben/examples/utils.py b/src/zepben/examples/utils.py index f8bcfc6..e64c8ba 100644 --- a/src/zepben/examples/utils.py +++ b/src/zepben/examples/utils.py @@ -26,17 +26,11 @@ def read_json_config(config_file_path: str) -> Dict: def eas_client_from_config(config: Dict, asynchronous: bool) -> EasClient: - """ - Builds an EasClient from an 'eas' config section. asynchronous is always a call-site - choice (the caller always wants True or always wants False), so it overrides whatever - an 'asynchronous' key in config might say, rather than colliding with it as a duplicate - kwarg. - """ config = {k: v for k, v in config.items() if k != "asynchronous"} return EasClient(**config, asynchronous=asynchronous) def get_client(config_dir, async_=True): config = read_json_config(f"{config_dir}/config.json")["eas"] - - return eas_client_from_config(config, async_) + config.update({"asynchronous": async_}) + return EasClient(**config)