Skip to content

Commit ce1046e

Browse files
authored
Merge pull request #10 from GRIDAPPSD/der-updates
Der updates
2 parents c720d78 + 183854b commit ce1046e

File tree

6 files changed

+358
-31
lines changed

6 files changed

+358
-31
lines changed

gui/webgui.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import asyncio
5+
import json
6+
import os.path
7+
import platform
8+
import shlex
9+
import sys
10+
import time
11+
import uuid
12+
from dataclasses import dataclass
13+
from datetime import datetime, timedelta
14+
from pathlib import Path
15+
from typing import Any, Optional
16+
17+
sys.path.insert(0, str(Path(__file__).parent.parent))
18+
19+
from nicegui import background_tasks, ui
20+
from session import backend_session, endpoint
21+
22+
import ieee_2030_5.models as m
23+
from ieee_2030_5.utils import dataclass_to_xml, xml_to_dataclass
24+
25+
tasks = []
26+
27+
debug = False
28+
29+
def _get_active(program):
30+
resp = backend_session.get(endpoint(f"derp/{program}/derca"))
31+
active_txt.value = resp.text
32+
33+
def _get_all_active():
34+
resp = backend_session.get(endpoint(f"derp"))
35+
derps: m.DERProgramList = xml_to_dataclass(resp.text)
36+
37+
value = ''
38+
for index, derps in enumerate(derps.DERProgram):
39+
resp = backend_session.get(endpoint(f"derp/{index}/derca"))
40+
active: m.DERControlList = xml_to_dataclass(resp.text)
41+
if active:
42+
value += f"\nProgram {index}\n"
43+
value += f"{active}"
44+
active_txt.value = value
45+
46+
def _submit_control_event(program, control):
47+
resp = backend_session.post(endpoint(f"derp/{program}/derc"),
48+
data=control,
49+
headers={"Content-Type": "application/xml"})
50+
_get_all_active()
51+
52+
def _show_text_area(label, value, only_when_debug=True):
53+
if debug == only_when_debug:
54+
ui.textarea(label=label, value=value).props('rows=20').props('cols=120').classes('w-full, h-80')
55+
56+
def get_control_event_default():
57+
derbase = m.DERControlBase(opModConnect=True, opModEnergize=False, opModFixedPFInjectW=80)
58+
59+
time_plus_10 = int(time.mktime((datetime.utcnow() + timedelta(seconds=10)).timetuple()))
60+
61+
derc = m.DERControl(mRID=str(uuid.uuid4()),
62+
description="New DER Control Event",
63+
DERControlBase=derbase,
64+
interval=m.DateTimeInterval(duration=20, start=time_plus_10))
65+
66+
67+
# setESLowVolt=0.917,
68+
# setESHighVolt=1.05,
69+
# setESLowFreq=59.5,
70+
# setESHighFreq=60.1,
71+
# setESRampTms=300,
72+
# setESRandomDelay=0,
73+
#DERControlBase=derbase)
74+
# dderc = m.DefaultDERControl(href=hrefs.get_dderc_href(),
75+
# mRID=str(uuid.uuid4()),
76+
# description="Default DER Control Mode",
77+
# setESDelay=300,
78+
# setESLowVolt=0.917,
79+
# setESHighVolt=1.05,
80+
# setESLowFreq=59.5,
81+
# setESHighFreq=60.1,
82+
# setESRampTms=300,
83+
# setESRandomDelay=0,
84+
# DERControlBase=derbase)
85+
return dataclass_to_xml(derc)
86+
87+
resp = backend_session.get(endpoint('derp'))
88+
derps: m.DERProgramList = xml_to_dataclass(resp.text)
89+
resp = backend_session.get(endpoint("enddevices"))
90+
enddevices: m.EndDeviceList = xml_to_dataclass(resp.text)
91+
92+
with_ders = filter(lambda x: x.DERListLink is not None, enddevices.EndDevice)
93+
94+
print([x for x in with_ders])
95+
96+
with ui.column():
97+
ui.label(f"# End Devices: {len(enddevices.EndDevice)}")
98+
_show_text_area("enddevices", dataclass_to_xml(enddevices))
99+
_show_text_area("derps", dataclass_to_xml(derps))
100+
101+
ui.label(f"# Derps: {len(derps.DERProgram)}")
102+
ui.label("FSA")
103+
for ed_index, ed in enumerate(enddevices.EndDevice):
104+
resp = backend_session.get(endpoint(f"edev/{ed_index}/fsa"))
105+
fsalist:m.FunctionSetAssignmentsList = xml_to_dataclass(resp.text)
106+
_show_text_area(f"edev/{ed_index}/fsa", resp.text)
107+
108+
for fsa_index, fsa in enumerate(fsalist.FunctionSetAssignments):
109+
resp = backend_session.get(endpoint(f"edev/{ed_index}/fsa/{fsa_index}"))
110+
_show_text_area(f"edev/{ed_index}/fsa/{fsa_index}", resp.text)
111+
112+
resp = backend_session.get(endpoint(f"edev/{ed_index}/fsa/{fsa_index}/derp"))
113+
_show_text_area(f"edev/{ed_index}/fsa/{fsa_index}/derp", resp.text)
114+
115+
select_list = {index: value.description for index, value in enumerate(derps.DERProgram)}
116+
117+
program = ui.select(options=select_list, value=list(select_list.keys())[0]).classes('w-full')
118+
xml_text = ui.textarea(label="xml", value=get_control_event_default()).props('rows=20').props('cols=120').classes('w-full, h-80')
119+
120+
ui.button("Assign DER Control Event", on_click=lambda: _submit_control_event(program.value, xml_text.value)).props('no-caps')
121+
ui.button("Show Active", on_click=lambda: _get_all_active()).props('no-caps')
122+
123+
active_txt = ui.textarea(label="Active").props("rows=20").props('cols=120').classes('w-full, h-80')
124+
#ui.select(options=[d for d in with_ders])
125+
126+
# def add_my_task(task):
127+
# tasks.append(task)
128+
129+
130+
# def _send_control_event():
131+
# default_pf = 0.99
132+
# import requests
133+
# session = requests.Session()
134+
# session.cert = ('/home/os2004/tls/certs/admin.pem', '/home/os2004/tls/private/admin.pem')
135+
# session.verify = "/home/os2004/tls/certs/ca.pem"
136+
137+
# control_path = Path('inverter.ctl')
138+
# derc: m.DERControl = xml_to_dataclass(xml_text.value)
139+
140+
141+
# time_now = int(time.mktime((datetime.utcnow()).timetuple()))
142+
143+
# while time_now < derc.interval.start:
144+
# time.sleep(0.1)
145+
# time_now = int(time.mktime((datetime.utcnow()).timetuple()))
146+
147+
# with open(str(control_path), 'wt') as fp:
148+
# fp.write(json.dumps(dict(pf=derc.DERControlBase.opModFixedPFInjectW)))
149+
150+
# while time_now < derc.interval.start + derc.interval.duration:
151+
# time.sleep(0.1)
152+
# time_now = int(time.mktime((datetime.utcnow()).timetuple()))
153+
154+
# control_path.write(json.dump(dict(pf=default_pf)))
155+
156+
157+
158+
159+
160+
# def _setup_event(element):
161+
# derbase = m.DERControlBase(opModConnect=True, opModEnergize=False, opModFixedPFInjectW=80)
162+
163+
# time_plus_60 = int(time.mktime((datetime.utcnow() + timedelta(seconds=60)).timetuple()))
164+
165+
# derc = m.DERControl(mRID=str(uuid.uuid4()),
166+
# description="New DER Control Event",
167+
# DERControlBase=derbase,
168+
# interval=m.DateTimeInterval(duration=10, start=time_plus_60))
169+
# element.value=dataclass_to_xml(derc)
170+
171+
172+
# async def _reset_tasks():
173+
# for task in tasks:
174+
# print(task.cancel())
175+
176+
# await asyncio.sleep(0.1)
177+
178+
# # while not task.cancelled():
179+
# # asyncio.sleep(0.1)
180+
# # print(task.cancelled())
181+
182+
183+
# tasks.clear()
184+
# agent_log.clear()
185+
# proxy_log.clear()
186+
# inverter_log.clear()
187+
188+
# #background_tasks.running_tasks.clear()
189+
190+
# async def run_command(command: LabeledCommand) -> None:
191+
# '''Run a command in the background and display the output in the pre-created dialog.'''
192+
193+
# process = await asyncio.create_subprocess_exec(
194+
# *shlex.split(command.command),
195+
# stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
196+
# cwd=command.working_dir
197+
# )
198+
199+
# # NOTE we need to read the output in chunks, otherwise the process will block
200+
# output = ''
201+
# while True:
202+
# new = await process.stdout.readline()
203+
# if not new:
204+
# break
205+
# output = new.decode()
206+
207+
# try:
208+
# jsonparsed = json.loads(output)
209+
# if command.output_element is not None:
210+
# command.output_element().push(output.strip())
211+
# except json.decoder.JSONDecodeError:
212+
# if not command.output_only_json:
213+
# command.output_element().push(output.strip())
214+
215+
# # NOTE the content of the markdown element is replaced every time we have new output
216+
# #result.content = f'```\n{output}\n```'
217+
218+
# with ui.dialog() as dialog, ui.card():
219+
# result = ui.markdown()
220+
221+
# @dataclass
222+
# class LabeledCommand:
223+
# label: str
224+
# command: str
225+
# output_element: Any
226+
# working_dir: str = str(Path(__file__).parent)
227+
# output_only_json: bool = True
228+
229+
# commands = [
230+
# LabeledCommand("Start Inverter", f'{sys.executable} inverter_runner.py', lambda: inverter_log),
231+
# LabeledCommand("Start Proxy", f'/home/os2004/repos/gridappsd-2030_5/.venv/bin/python -m ieee_2030_5.basic_proxy config.yml ', lambda: proxy_log, "/home/os2004/repos/gridappsd-2030_5",
232+
# output_only_json=False),
233+
# LabeledCommand("Start Agent", f'{sys.executable} -m ieee_2030_5.agent', lambda: agent_log, "/home/os2004/repos/volttron/services/core/IEEE_2030_5",
234+
# output_only_json=False),
235+
# ]
236+
237+
# with ui.column():
238+
# # commands = [f'{sys.executable} inverter_runner.py']
239+
# with ui.row():
240+
241+
# for command in commands:
242+
# ui.button(command.label, on_click=lambda _, c=command: add_my_task(background_tasks.create(run_command(c)))).props('no-caps')
243+
244+
# ui.button("Reset", on_click=lambda: _reset_tasks()).props('no-caps')
245+
# #ui.button("Update Control Time", on_click=lambda: _setup_event(xml_text)).props('no-caps')
246+
# #ui.button("Send Control", on_click=lambda: _send_control_event()).props('no-caps')
247+
# # with ui.row():
248+
# # xml_text = ui.textarea(label="xml", value=get_control_event_default()).props('rows=20').props('cols=120').classes('w-full, h-80')
249+
# with ui.row():
250+
# ui.label("Inverter Log")
251+
# inverter_log = ui.log(10).props('rows=5').props('cols=120').classes('w-full h-80')
252+
# with ui.row():
253+
# ui.label("Proxy Log")
254+
# proxy_log = ui.log(10).props('rows=5').props('cols=120').classes('w-full h-80')
255+
# with ui.row():
256+
# ui.label("Agent Log")
257+
# agent_log = ui.log(10).props('rows=5').props('cols=120').classes('w-full h-80')
258+
259+
260+
# NOTE on windows reload must be disabled to make asyncio.create_subprocess_exec work (see https://github.com/zauberzeug/nicegui/issues/486)
261+
ui.run(reload=platform.system() != "Windows")

ieee_2030_5/adapters/der.py

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -312,34 +312,42 @@ def __init__(self) -> None:
312312
self._der_programs: List[DERProgram] = []
313313
self._der_default_control: Dict[int, m.DefaultDERControl] = {}
314314
self._der_control: Dict[int, List[m.DERControl]] = {}
315+
self._der_active: Dict[int, List[m.DERControl]] = {}
315316
TimeAdapter.tick.connect(self._time_updated)
316317

317318
def _time_updated(self, timestamp):
318-
319-
for indx, ctrl_list in self._der_control.items():
320-
event_removed = []
321-
for ctrl_index, ctrl in enumerate(ctrl_list):
319+
for derp_index, derp in enumerate(self._der_programs):
320+
if not self._der_active.get(derp_index):
321+
self._der_active[derp_index] = []
322+
for ctrl_index, ctrl in enumerate(self._der_control.get(derp_index, [])):
322323
if not ctrl.EventStatus:
323-
if ctrl.interval.start > timestamp and timestamp < ctrl.interval.start + ctrl.interval.duration:
324+
_log.debug(f"Setting up event for ctrl {ctrl_index} current_time: {timestamp} start_time: {ctrl.interval.start}")
325+
if timestamp > ctrl.interval.start and timestamp < ctrl.interval.start + ctrl.interval.duration:
324326
ctrl.EventStatus = m.EventStatus(currentStatus=1, dateTime=timestamp, potentiallySuperseded=False, reason="Active")
327+
self._der_active[derp_index].append(ctrl)
325328
else:
326329
ctrl.EventStatus = m.EventStatus(currentStatus=0, dateTime=timestamp, potentiallySuperseded=False, reason="Scheduled")
327-
330+
_log.debug(f"ctrl.EventStatus is {ctrl.EventStatus}")
331+
328332
# Active control
329333
if ctrl.interval.start < timestamp and timestamp < ctrl.interval.start + ctrl.interval.duration:
330-
# TODO send active control event
331334
if ctrl.EventStatus.currentStatus == 0:
335+
_log.debug(f"Activating control {ctrl_index}")
332336
ctrl.EventStatus.currentStatus = 1 # Active
333337
ctrl.EventStatus.dateTime = timestamp
334338
ctrl.EventStatus.reason = f"Control event active {ctrl.mRID}"
339+
if ctrl.mRID not in [x.mRID for x in self._der_active[derp_index]]:
340+
self._der_active[derp_index].append(ctrl)
335341

336342
elif timestamp > ctrl.interval.start + ctrl.interval.duration:
337-
if ctrl.EventStatus.currentStatus == 1: # Only remove if we were active (otherwise we need to delete differently)
338-
event_removed.append(ctrl_index)
339-
340-
for i in sorted(event_removed, reverse=True):
341-
ctrl_list.pop(i)
342-
343+
if ctrl.EventStatus.currentStatus == 1:
344+
_log.debug(f"Deactivating control {ctrl_index}")
345+
ctrl.EventStatus.currentStatus = -1 # for me this means complete
346+
for index, c in enumerate(self._der_active[derp_index]):
347+
if c.mRID == ctrl.mRID:
348+
self._der_active[derp_index].pop(index)
349+
break
350+
343351

344352

345353

@@ -446,7 +454,7 @@ def fetch_der_control_list(self, program_index: int, start: int = 0, after: int
446454
return m.DERControlList(href=hrefs.der_program_href(program_index, hrefs.DERProgramSubType.DERControlListLink), all=all, results=all,
447455
DERControl=der_controls)
448456

449-
def fetch_der_active_control_list(self, program_index: int, int, start: int = 0, after: int = 0, limit: int = 0) -> m.DERControlList:
457+
def fetch_der_active_control_list(self, program_index: int, start: int = 0, after: int = 0, limit: int = 0) -> m.DERControlList:
450458
der_control_list = m.DERControlList(href=hrefs.der_program_href(program_index, hrefs.DERProgramSubType.ActiveDERControlListLink),
451459
DERControl=self.get_der_active_controls(program_index=program_index))
452460

@@ -457,8 +465,9 @@ def fetch_der_active_control_list(self, program_index: int, int, start: int = 0,
457465
def get_der_active_controls(self, program_index: int) -> List[m.DERControl]:
458466
lst: List[m.DERControl] = []
459467
for ctrl in self._der_control.get(program_index, []):
460-
if ctrl.EventStatus.currentStatus == 1: # Active
461-
lst.append(ctrl)
468+
if ctrl.EventStatus:
469+
if ctrl.EventStatus.currentStatus == 1: # Active
470+
lst.append(ctrl)
462471
return lst
463472

464473
def get_default_der_control(self, program_index) -> m.DefaultDERControl:

0 commit comments

Comments
 (0)