Skip to content

Commit 0aaf817

Browse files
authored
Downstream version of DEV-3394 (#24)
Signed-off-by: Max Chesterfield <max.chesterfield@zepben.com>
1 parent cf2aa13 commit 0aaf817

2 files changed

Lines changed: 144 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,5 @@ docs/build
133133
src/zepben/examples/config.json
134134

135135
*.crt
136+
137+
src/zepben/examples/csvs/
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
# Copyright 2025 Zeppelin Bend Pty Ltd
2+
#
3+
# This Source Code Form is subject to the terms of the Mozilla Public
4+
# License, v. 2.0. If a copy of the MPL was not distributed with this
5+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
6+
7+
import asyncio
8+
import json
9+
import os
10+
from dataclasses import dataclass
11+
from typing import Dict
12+
13+
import pandas as pd
14+
from zepben.evolve import NetworkConsumerClient, connect_with_token, Tracing, EnergyConsumer, PowerTransformer, \
15+
TransformerFunctionKind, Breaker, Fuse, IdentifiedObject, EquipmentTreeBuilder, downstream, TreeNode, Feeder
16+
from zepben.protobuf.nc.nc_requests_pb2 import IncludedEnergizedContainers
17+
18+
19+
@dataclass
20+
class EnergyConsumerDeviceHierarchy:
21+
energy_consumer_mrid: str
22+
lv_circuit_name: str
23+
upstream_switch_mrid: str
24+
lv_circuit_name: str
25+
upstream_switch_class: str
26+
distribution_power_transformer_mrid: str
27+
distribution_power_transformer_name: str
28+
regulator_mrid: str
29+
breaker_mrid: str
30+
feeder_mrid: str
31+
32+
33+
def _get_client():
34+
with open('config.json') as f:
35+
config = json.load(f)
36+
37+
# Connect to server
38+
channel = connect_with_token(
39+
host=config["host"],
40+
access_token=config["access_token"],
41+
rpc_port=config['rpc_port'],
42+
ca_filename=config['ca_path']
43+
)
44+
return NetworkConsumerClient(channel)
45+
46+
47+
async def get_feeders() -> Dict[str, Feeder]:
48+
_feeders = (await _get_client().get_network_hierarchy()).result.feeders
49+
return _feeders
50+
51+
52+
def process_leaf(up_data: dict, leaf: TreeNode):
53+
to_equip: IdentifiedObject = leaf.identified_object
54+
55+
if isinstance(to_equip, Breaker):
56+
if not up_data.get('breaker'):
57+
up_data['breaker'] = to_equip
58+
elif isinstance(to_equip, Fuse):
59+
if not up_data.get('upstream_switch'):
60+
up_data['upstream_switch'] = to_equip
61+
elif isinstance(to_equip, PowerTransformer):
62+
if not up_data.get('distribution_power_transformer'):
63+
up_data['distribution_power_transformer'] = to_equip
64+
elif not up_data.get('regulator') and to_equip.function == TransformerFunctionKind.voltageRegulator:
65+
up_data['regulator'] = to_equip
66+
67+
68+
async def trace_from_feeder(feeder_mrid: str):
69+
"""
70+
Fetch the equipment container from the given feeder and build an equipment tree of everything downstream of the feeder.
71+
Use the Equipment tree to traverse upstream of all EC's and get the equipment we are interested in.
72+
Finally, create a CSV with the relevant information.
73+
"""
74+
client = _get_client()
75+
print(f'processing feeder {feeder_mrid}')
76+
77+
# Get all objects under the feeder, including Substations and LV Feeders
78+
await client.get_equipment_container(
79+
feeder_mrid,
80+
include_energized_containers = IncludedEnergizedContainers.INCLUDE_ENERGIZED_LV_FEEDERS
81+
)
82+
83+
feeder = client.service.get(feeder_mrid, Feeder)
84+
85+
builder = EquipmentTreeBuilder()
86+
87+
await (
88+
Tracing.network_trace()
89+
.add_condition(downstream())
90+
.add_step_action(builder)
91+
).run(getattr(feeder, 'normal_head_terminal'))
92+
93+
energy_consumers = []
94+
for up in client.service.objects(EnergyConsumer):
95+
# iterate up tree from EC.
96+
up_data = {'feeder': feeder.mrid, 'energy_consumer_mrid': up.mrid}
97+
def _process(leaf):
98+
process_leaf(up_data, leaf)
99+
if leaf.parent:
100+
_process(leaf.parent)
101+
try:
102+
_process(builder.leaves[up.mrid])
103+
except KeyError:
104+
# If the up is not in the Equipment tree builders leaves, skip it
105+
continue
106+
107+
row = _build_row(up_data)
108+
energy_consumers.append(row)
109+
110+
csv_sfx = "energy_consumers.csv"
111+
network_objects = pd.DataFrame(energy_consumers)
112+
os.makedirs("csvs", exist_ok=True)
113+
network_objects.to_csv(f"csvs/{feeder.mrid}_{csv_sfx}", index=False)
114+
115+
116+
class NullEquipment:
117+
"""empty class to simplify code below in the case of an equipment not existing in that position of the network"""
118+
mrid = None
119+
name = None
120+
121+
122+
def _build_row(up_data: dict[str, IdentifiedObject | str]) -> EnergyConsumerDeviceHierarchy:
123+
return EnergyConsumerDeviceHierarchy(
124+
energy_consumer_mrid = up_data['energy_consumer_mrid'],
125+
upstream_switch_mrid = (up_data.get('upstream_switch') or NullEquipment).mrid,
126+
lv_circuit_name = (up_data.get('upstream_switch') or NullEquipment).name,
127+
upstream_switch_class = type(up_data.get('upstream_switch')).__name__,
128+
distribution_power_transformer_mrid = (up_data.get('distribution_power_transformer') or NullEquipment).mrid,
129+
distribution_power_transformer_name = (up_data.get('distribution_power_transformer') or NullEquipment).name,
130+
regulator_mrid = (up_data.get('regulator') or NullEquipment).mrid,
131+
breaker_mrid = (up_data.get('breaker') or NullEquipment).mrid,
132+
feeder_mrid = up_data.get('feeder'),
133+
)
134+
135+
136+
if __name__ == "__main__":
137+
# Get a list of feeders before entering main compute section of script.
138+
feeders = asyncio.run(get_feeders())
139+
140+
print('processing feeders')
141+
for _feeder in feeders:
142+
asyncio.run(trace_from_feeder(_feeder))

0 commit comments

Comments
 (0)