From 8a478e228116e102b5e0d8f7d0371b585bdfc330 Mon Sep 17 00:00:00 2001 From: "David P. Chassin" Date: Tue, 30 May 2023 19:06:22 -0500 Subject: [PATCH] Add server subcommand and gldserver module (#1292) * (#1277) Added additional check and errors for non-SPCT transformers that are connected to triplex_node objects, or have an "S" phase. (#1281) (#37) Migration of fix from gridlabd issue 1432 Co-authored-by: Frank Tuffner * Add server python module * Update Makefile.mk * Fix naming conflict. * Fix resource warning (one of two) * Update server.cpp * Update object.cpp * Update gridlabd-server * Update gldserver.py * Update gldserver.py * Update gridlabd-server * Update Makefile.am * Update gridlabd-server * Update regulator.cpp * Update server.cpp * Update gldserver.py --------- Co-authored-by: Frank Tuffner --- Makefile.am | 2 +- source/main.cpp | 4 +- source/object.cpp | 10 +- source/server.cpp | 42 +++- subcommands/Makefile.mk | 1 + subcommands/gridlabd-server | 227 ++++++++++++++++++++ tools/Makefile.mk | 1 + tools/gldserver.py | 417 ++++++++++++++++++++++++++++++++++++ tools/test_server.glm | 5 + 9 files changed, 695 insertions(+), 14 deletions(-) create mode 100644 subcommands/gridlabd-server create mode 100644 tools/gldserver.py create mode 100644 tools/test_server.glm diff --git a/Makefile.am b/Makefile.am index d51faa7e2..cb74b1bbb 100644 --- a/Makefile.am +++ b/Makefile.am @@ -130,7 +130,7 @@ $(PYENV): requirements.txt @python$(PYVER)-config --prefix 1>/dev/null || ( echo "ERROR [Makefile]: python$(PYVER)-dev is not installed" > /dev/stderr ; false ) @echo "Processing python requirements..." @mkdir -p $(PYBIN) $(PYLIB) $(PYINC) - @(deactivate >/dev/null || true ; $(SYSPYTHON) -m venv $(PYENV)) + @(deactivate 1>/dev/null 2>&1 || true ; $(SYSPYTHON) -m venv $(PYENV)) @$(ENVPYTHON) --version 1>/dev/null || ( echo "ERROR [Makefile]: $(ENVPYTHON) is not found" > /dev/stderr ; false ) @$(ENVPYTHON) -m pip install --upgrade pip @$(ENVPYTHON) -m pip install pandas==2.0.0 diff --git a/source/main.cpp b/source/main.cpp index 9f144f3a1..6fb8156f5 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -740,8 +740,8 @@ int ppolls(struct s_pipes *pipes, FILE* input_stream, FILE* output_stream, FILE if ( polldata[0].revents&POLLNVAL ) { // fprintf(stderr,"poll() pipe 0 invalid\n"); fflush(stderr); - output_error("GldMain::subcommand(command='%s'): input pipe invalid", pipes->child_command); - has_error = true; + output_warning("GldMain::subcommand(command='%s'): input pipe invalid", pipes->child_command); + // has_error = true; break; } if ( polldata[1].revents&POLLNVAL ) diff --git a/source/object.cpp b/source/object.cpp index f449a1179..4251aaa73 100644 --- a/source/object.cpp +++ b/source/object.cpp @@ -2461,27 +2461,27 @@ int object_property_getsize(OBJECT *obj, PROPERTY *prop) // dynamic size PROPERTYSPEC *spec = property_getspec(prop->ptype); int len = spec->csize; - IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s','type':'%s'}): prop->width = %d", object_name(obj), prop->name, property_getspec(prop->ptype)->name, len); + // IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s','type':'%s'}): prop->width = %d", object_name(obj), prop->name, property_getspec(prop->ptype)->name, len); if ( len == PSZ_DYNAMIC ) { len = property_write(prop,(char*)(obj+1)+(int64_t)(prop->addr),NULL,0); - IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = PSZ_DYNAMIC => len = %d", object_name(obj), prop->name, len); + // IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = PSZ_DYNAMIC => len = %d", object_name(obj), prop->name, len); } else if ( len == PSZ_AUTO ) { // TODO: support general calls to underlying class implementing the property std::string *str = (std::string*)(char*)(obj+1)+(int64_t)(prop->addr); len = str->size()+1; - IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = PSZ_AUTO => len = %d", object_name(obj), prop->name, len); + // IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = PSZ_AUTO => len = %d", object_name(obj), prop->name, len); } if ( len < 0 ) { - IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = %d => len = 0", object_name(obj), prop->name, len); + // IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = %d => len = 0", object_name(obj), prop->name, len); len = 0; } else { - IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = %d", object_name(obj), prop->name, len); + // IN_MYCONTEXT output_debug("object_property_getsize(OBJECT *obj={'name':'%s'}, PROPERTY *prop={'name':'%s'}) -> len = %d", object_name(obj), prop->name, len); } return len; diff --git a/source/server.cpp b/source/server.cpp index a2665f7b9..7b3ea7cee 100644 --- a/source/server.cpp +++ b/source/server.cpp @@ -1124,17 +1124,15 @@ int http_find_request(HTTPCNX *http,char *uri) return 0; http_format(http,"[\n"); obj = find_first(list); - while ( 1 ) + while ( obj ) { if ( obj->name == NULL ) http_format(http,"\t{\"name\" : \"%s:%d\"}",obj->oclass->name,obj->id); else http_format(http,"\t{\"name\" : \"%s\"}",obj->name); obj = find_next(list,obj); - if ( obj!=NULL ) + if ( obj != NULL ) http_format(http,",\n\t"); - else - break; } http_format(http,"\n\t]\n"); http_type(http,"text/json"); @@ -1207,6 +1205,37 @@ int http_read_request(HTTPCNX *http, char *uri) return 1; } +/** Process a utility operation + * @returns non-zero on success, 0 on failure (errno set) + **/ + +int http_util_request(HTTPCNX *http, char *uri) +{ + char token[64], value[1024]; + if ( sscanf(uri,"%[^/]/%[^\n]",token,value) < 2 ) + { + return 1; + } + if ( strcmp(token,"convert_to_timestamp") == 0 ) + { + http_type(http,"text/html"); + http_decode(value); + http_format(http,"%lld",convert_to_timestamp(value)); + return 1; + } + else if ( strcmp(token,"convert_from_timestamp") == 0 ) + { + http_type(http,"text/html"); + char buffer[64]; + http_format(http,"%s",convert_from_timestamp(atol(value),buffer,sizeof(buffer)-1)>0?buffer:"INVALID"); + return 1; + } + else + { + http_format(http,"uri '%s/%s' is not valid",token,value); + return 9; + } +} /** Process an incoming GUI request @returns non-zero on success, 0 on failure (errno set) **/ @@ -2023,9 +2052,10 @@ void *http_response(void *ptr) {"/octave/", http_run_octave, HTTP_OK, HTTP_NOTFOUND}, {"/kml/", http_kml_request, HTTP_OK, HTTP_NOTFOUND}, {"/json/", http_json_request, HTTP_OK, HTTP_NOTFOUND}, - {"/find/", http_find_request, HTTP_OK, HTTP_NOTFOUND}, + {"/find/", http_find_request, HTTP_OK, HTTP_NOTFOUND}, {"/modify/", http_modify_request, HTTP_OK, HTTP_NOTFOUND}, - {"/read/", http_read_request, HTTP_OK, HTTP_NOTFOUND}, + {"/read/", http_read_request, HTTP_OK, HTTP_NOTFOUND}, + {"/util/", http_util_request, HTTP_OK, HTTP_NOTFOUND}, }; size_t n; for ( n=0 ; n /dev/stderr + exit $RC +} + +function getport () +{ + if [ $# -eq 0 ]; then + PORT=6267 + while [ ! -z "$(curl -s http://localhost:$PORT/raw/mainloop_state 2>/dev/null)" ]; do + PORT=$(($PORT+1)) + done + echo $PORT + else + echo $(($1+6266)) + fi +} + +function getstatus() +{ + for P in $(list $*); do + echo $P $(curl -s http://localhost:$P/raw/mainloop_state 2>/dev/null || echo "NOREPLY") + done + +} + +function waitport () +{ + for P in $(list $*); do + T=0 + while [ ! -z "$(curl -s http://localhost:$P/raw/mainloop_state 2>/dev/null)" ]; do + sleep 1 + T=$(($T+1)) + if [ $T -gt $TIMEOUT ]; then + error E_FAILED "wait on port $P timeout" + fi + done + done +} + +function list () +{ + if [ $# -gt 0 ]; then + for P in $*; do + curl -s http://localhost:$P/raw/mainloop_state >/dev/null && echo $P + done + else + P=0 + while [ $P -lt $MAXPORTS ]; do + PORT=$(($P+6267)) + curl -s http://localhost:$PORT/raw/mainloop_state >/dev/null && echo $PORT + P=$(($P+1)) + done + fi +} + +# catch no args +if [ $# -eq 0 ]; then + grep '^## ' $0 | cut -c4- | grep '^Syntax: ' > /dev/stderr + exit $E_SYNTAX +fi + +# process args +case $1 in + help | --help | -h ) + grep '^## ' $0 | cut -c4- + ;; + start ) + shift 1 + while [ -z "$OK" ]; do + case $1 in + -p | --port ) + PORT=$2 + shift 2 + ;; + -d | --detach ) + DETACH=yes + shift 1 + ;; + -l | --logfile ) + LOG=$2 + shift 2 + ;; + * ) + OK=yes + ;; + esac + done + if [ -z "$PORT" ]; then + PORT=$(getport) + echo $PORT + fi + if [ -z "$LOG" ]; then + LOG=$LOGFILE-$PORT.log + elif [ "$LOG" = "-" ]; then + LOG=/dev/stderr + fi + rm -f $LOG + gridlabd --server -D server_portnum=$PORT $* 1>$LOG 2>&1 & + sleep 1 + if [ ! -z "$!" ] ; then + if [ ! -z "$DETACH" ]; then + disown %1 + fi + else + error $E_FAILED "unable to start server for port $PORT" + fi + ;; + stop | halt | shutdown | kill ) + if [ -z "$2" ]; then + error $E_MISSING "missing port number" + elif [ "$2" = "all" ]; then + PORTLIST=$(list) + else + PORTLIST=$* + fi + for PORT in $PORTLIST; do + curl -s http://localhost:$PORT/control/$1 + done + waitport $PORTLIST + ;; + list ) + shift 1 + list $* + ;; + log ) + shift 1 + if [ $# -eq 0 ]; then + error $E_MISSING "missing port number" + elif [ ! -f $LOGFILE-$1.log ]; then + error $E_NOTFOUND "no log found for port $1" + fi + echo 'INFO [gridlabd-server]: created '$(stat -f %SB -t "%Y-%m-%d %H:%M:%S %Z" $LOGFILE-$1.log) + cat $LOGFILE-$1.log + if [ -z "$(getstatus $1)" ]; then + echo 'INFO [gridlabd-server]: stopped '$(stat -f %Sm -t "%Y-%m-%d %H:%M:%S %Z" $LOGFILE-$1.log) + else + echo 'INFO [gridlabd-server]: updated '$(stat -f %Sm -t "%Y-%m-%d %H:%M:%S %Z" $LOGFILE-$1.log) + fi + ;; + status ) + shift 1 + if [ "$1" = "-c" -o "$1" = "--continuous" ]; then + shift 1 + while [ true ]; do + clear + echo "Server Last update Status Options" + echo "------ ------------------- ---------- --------------------" + for P in $(list $*); do + printf '%6.6s %19.19s %10.10s %s\n' "$P" "$(stat -f %Sm -t "%Y-%m-%d %H:%M:%S" $LOGFILE-$P.log)" "$(curl -s http://localhost:$P/raw/mainloop_state 2>/dev/null || echo 'NOREPLY')" "$(curl -s http://localhost:$P/raw/command_line 2>/dev/null | cut -f5- -d' ')" + done + echo "" + echo "Press Ctrl-C to quit" + sleep 1 + done + else + getstatus $* + fi + ;; + * ) + error $E_INVALID "'$1' is an invalid gridlabd-server command" + ;; +esac +exit $E_OK + diff --git a/tools/Makefile.mk b/tools/Makefile.mk index 2485a0fb6..5bf8e9201 100644 --- a/tools/Makefile.mk +++ b/tools/Makefile.mk @@ -18,6 +18,7 @@ dist_pkgdata_DATA += tools/market_model.py dist_pkgdata_DATA += tools/meteostat_weather.py dist_pkgdata_DATA += tools/metar2glm.py dist_pkgdata_DATA += tools/read_dlp.py +dist_pkgdata_DATA += tools/gldserver.py dist_pkgdata_DATA += tools/noaa_forecast.py dist_pkgdata_DATA += tools/nsrdb_weather.py dist_pkgdata_DATA += tools/ucar_weather.py diff --git a/tools/gldserver.py b/tools/gldserver.py new file mode 100644 index 000000000..0069fb227 --- /dev/null +++ b/tools/gldserver.py @@ -0,0 +1,417 @@ +"""GridLAB-D server library + +Access a GridLAB-D simulation while it is running using REST API. + +Examples +-------- + +1. Start a detached server and get the version: + + $ gridlabd python + >>> from gldserver import GridlabdServer + >>> sim = GridlabdServer(modelname) + >>> print(sim.get_global("version")) + +2. Start a server in a context and get the version: + + $ gridlabd python + >>> from gldserver import GridlabdServer + >>> with GridlabdServer(modelname) as sim: + ... print(sim.get_global("version")) + +3. Read an object property as a string + + value = sim.get_property(objname,propname) + +4. Read an object property as a double and print its unit + + value = sim.get_property(objname,propname,astype=GldDouble) + print(value.unit) + +5. Read an object property as a complex and print its real part + + value = sim.get_property(objname,propname,astype=GldComplex) + print(value.real) + +6. Set a property + + sim.set_property(objname,propname,value) +""" + +import sys, os +import requests +import subprocess +import json +import time +import datetime +import math, cmath +import tempfile +import datetime, pytz + +# +# Module I/O +# +VERBOSE = False # extra output +QUIET = False # no warning output +SILENT = False # no error output + +def verbose(msg): + """Print a verbose message""" + if VERBOSE: + print(f"VERBOSE [gldserver {datetime.datetime.now()}]: {msg}",file=sys.stderr,flush=True) + +def error(msg): + """Print an error message""" + if not SILENT: + print(f"ERROR [gldserver {datetime.datetime.now()}]: {msg}",file=sys.stderr,flush=True) + +def warning(msg): + """Print a warning message""" + if not QUIET: + print(f"WARNING [gldserver {datetime.datetime.now()}]: {msg}",file=sys.stderr,flush=True) + +def exception(msg): + """Raise an exception""" + raise GridlabdServerException(msg) + +# +# GridLAB-D property types +# +class GldDouble(float): + """GridLAB-D double property + + Properties: + + unit (str) Unit in which value is represented + """ + + def __new__(self,value): + if type(value) is str: + y = value.split(' ') + return float.__new__(self,y[0]) + else: + return float.__new__(x) + + def __init__(self,value): + if type(value) is str: + y = value.split(' ') + float.__init__(y[0]) + self.unit = y[1] if len(y) > 1 else None + else: + float.__init__(x) + +class GldComplex(complex): + """GridLAB-D complex property + + Properties: + + unit (str) Unit in which value is represented + """ + + def __new__(self,value,y=None): + if type(value) is str: + y = value.split(' ') + d = y[0][-1] + if d in "ij": + return complex.__new__(self,f"{y[0][:-1]}j") + elif d == 'd': + z = complex(f"{y[0][:-1]}j") + return complex.__new__(self,cmath.rect(z.real,z.imag*math.pi/180)) + elif d == 'a': + z = complex(f"{y[0][:-1]}j") + return complex.__new__(self,cmath.rect(z.real,z.imag)) + else: + raise ValueError("invalid complex number") + self.unit = y[1] if len(y) > 1 else None + else: + return complex.__new__(self,value,y) + + def __init__(self,x,y=None): + if type(x) is str: + y = x.split(' ') + self.unit = y[1] if len(y) > 1 else None + else: + complex.__init__(x,y) + + def __str__(self): + return f"{self.real:+f}{self.imag:+f}j" + +class GldTimestamp(datetime.datetime): + """GridLAB-D timestamp property + + Properties: + + tz () + """ + tz = None + url = None + fmt = "%Y-%m-%d %H:%M:%S %Z" + def __new__(self,*args): + dt = self.parse(*args) + return datetime.datetime.__new__(self,dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,tzinfo=self.tz) + + def __init__(self,*args): + dt = self.parse(*args) + print(f"GldTimestamp.__init__({args}) --> {dt}",file=sys.stderr,flush=True) + datetime.datetime.__init__(dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,tzinfo=self.tz) + + def format(self,fmt=None): + return self.strftime(fmt if fmt else self.fmt) + + @classmethod + def parse(self,*args): + if len(args) == 1: + if type(args[0]) is str: + url = f"{self.url}/util/convert_to_timestamp/{args[0].replace(' ','%20')}" + # breakpoint() + reply = requests.get(url) + if reply.status_code == 200: + print(args[0],'-->',reply.status_code,reply.text,file=sys.stderr,flush=True) + return datetime.datetime.fromtimestamp(int(reply.text)) + print(args[0],'-->',reply.status_code,file=sys.stderr,flush=True) + return None + elif type(args[0]) is int: + return datetime.datetime.fromtimestamp(args[0]) + else: + return datetime.datetime(*args) + exception("invalid date/time type") + +# +# Server implementation +# +class GridlabdServerException(Exception): + """General GridLAB-D server exception""" + pass + +class GridlabdServer: + """GridLAB-D server""" + + HOST = "localhost" # default host to use when connecting + PROTOCOL = "http" # default protocol to use when connecting + TIMEOUT = 5.0 # default timeout to use when starting/stopping + RETRYTIME = 0.1 # initial retry time to use when starting/stopping + LOGFILE = None # file in which to store simulation output (or None, or subprocess.PIPE) + + def __init__(self,*args,detached=True): + """Start a server + + Properties: + + args Command options for GridLAB-D + + detached Specify whether server remains attached to this + process (default True) + + Exceptions: + + GridlabdServerException Unable to start server + """ + self.status = None + self.args = args + self.port = None + if len(args) > 0: + self.start(*args,detached=detached) + + def __del__(self): + """Cleanup + + Exceptions: + + GridlabdServerException Unable to stop server + """ + self.stop() + + def __enter__(self,*args): + """Start a server in context + + Properties: + + args Command options for GridLAB-D + """ + return self + + def __exit__(self,*args): + """Stop a server in context + + Exceptions: + + GridlabdServerException Unable to stop server + """ + self.stop() + + def start(self,*args,detached=False): + """Start the server + + Arguments: + + *args GridLAB-D command line arguments + + detached Specify whether the server should remain attached to + this process (default False) + + Exceptions: + + GridlabdServerException Unable to start server + + Exception All other exceptions + """ + if (status:=self.wait("RUNNING")) == "RUNNING": + if detached: + exception(f"cannot attach to process on port {self.port}") + version = self.getversion() + verbose(f"server version '{version}' ok") + self.proc = None + elif status: + exception(f"server up but not running (status={status})") + else: + cmd = ["gridlabd","server","start"] + if not self.port is None: + cmd.extend(["-p",str(self.port)]) + if detached: + cmd.append("--detach") + cmd.extend(["-D","show_progress=FALSE"]) + cmd.extend(args) + verbose(f"starting {cmd}") + self.proc = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + time.sleep(1) + self.port = self.proc.stdout.readline().strip().decode('utf-8') + verbose(f"started ok on port {self.port}") + if self.wait("RUNNING") != "RUNNING": + exception(f"server start timeout") + version = self.getversion() + verbose(f"server version {version} up") + GldTimestamp.tz = pytz.timezone(self.get_global("timezone_locale")) + GldTimestamp.url = f"http://localhost:{self.port}" + + def getstatus(self): + self.status = self.query("raw","mainloop_state",onfail=None) + return self.status + + def getversion(self): + self.version = self.query("raw","version",onfail=None) + return self.version + + def wait(self,status=None): + timer = 0 + retry = self.RETRYTIME + while (actual:=self.getstatus()) != status and timer < self.TIMEOUT: + time.sleep(retry) + timer += retry + retry *= 2 if retry < 5 else 1 + return actual + + def stop(self): + """Stop the server normally + + Exceptions: + + GridlabdServerException Unable to start server and get version + """ + verbose("stopping server") + self.query("control","stop",onfail=None) + if not self.wait() is None: + exception("unable to stop server") + elif self.proc: + self.proc.wait() + self.proc.stdout.close() + self.proc.stderr.close() + + def query(self,*args,astype=str,onfail=verbose): + """Send a query to the server REST API""" + if self.port is None: + return None + url = f"{self.PROTOCOL}://{self.HOST}:{self.port}/{'/'.join(args)}" + try: + result = requests.get(url) + self.status = result.status_code + verbose(f"query '{url}' --> '{result.text}' (code {result.status_code})") + return astype(result.text) if result.status_code == 200 else None + except Exception as err: + self.status = sys.exc_info() + verbose(f"query '{url}' --> {self.status[0].__name__}") + if onfail: + onfail(err) + return None + + def get_global(self,name,astype=str): + """Get a global variable from the simulation""" + return self.query("raw",name,astype=astype) + + def set_global(self,name,value): + """Set a global variable in the simulation""" + self.query("raw",f"{name}={value}") + + def get_property(self,obj,name,astype=str): + """Get an object property""" + return self.query("raw",obj,name,astype=astype) + + def set_property(self,obj,name,value): + """Set an object property""" + self.query("modify",f"{obj}.{name}={value}") + + def get_objects(self,collection): + """Get a list of objects from a collection""" + result = self.query("find",collection) + return [x['name'] for x in json.loads(result)] if result else None + +# +# Unit testing +# +if __name__ == "__main__": + + import unittest + import tracemalloc + tracemalloc.start() + + with tempfile.NamedTemporaryFile(suffix=".glm",mode="w+t",delete=True) as fh: + + fh.write(f"""// created by {sys.argv} unittest on {datetime.datetime.now()} +#option warn +#ifmissing 123.glm +#model get IEEE/123 +#endif +#include "123.glm" +#ifmissing "CA-San_Francisco_Intl_Ap.glm" +#weather get "CA-San_Francisco_Intl_Ap.tmy3" +#endif +#input "CA-San_Francisco_Intl_Ap.tmy3" +#set run_realtime=1 +""") + TMPFILE = fh.name + class TestServer(unittest.TestCase): + + # def test_detached(self): + # """Verify that server can be started detached""" + # fh.seek(0) + # sim = GridlabdServer(fh.name,detached=True) + # self.assertEqual(len(sim.get_objects("class=load")),85) + # sim.stop() + + def test_attached(self): + """Verify that server can be started attached""" + fh.seek(0) + sim = GridlabdServer(fh.name,detached=False) + now = datetime.datetime.now(GldTimestamp.tz) + dt = sim.get_global("clock",astype=GldTimestamp) + self.assertEqual(dt.format(),now.strftime(GldTimestamp.fmt)) + del sim + + # def test_context(self): + # """Verify that server can be started in a context""" + # fh.seek(0) + # with GridlabdServer(fh.name) as sim: + # self.assertEqual(len(sim.get_objects("class=load")),85) + # self.assertEqual(sim.get_property("node_14","bustype"),"SWING") + # self.assertEqual(sim.get_property("node_14","voltage_A",astype=GldComplex).real,2401.78) + # sim.set_property("load_1","constant_power_A",GldComplex(40000,20000)) + # time.sleep(2) + # self.assertEqual(sim.get_property("load_1","constant_power_A",astype=GldComplex),GldComplex(40000,20000)) + # self.assertEqual(round(sim.get_property("load_1","voltage_A",astype=GldComplex).real,1),2384.8) + # sim.set_property("load_1","constant_power_A",GldComplex(50000,25000)) + # time.sleep(2) + # self.assertEqual(sim.get_property("load_1","constant_power_A",astype=GldComplex),GldComplex(50000,25000)) + # self.assertEqual(round(sim.get_property("load_1","voltage_A",astype=GldComplex).real,1),2384.5) + + unittest.main() + if TMPFILE and os.path.exists(TMPFILE): + warning(f"temporary file {TMPFILE} was not deleted after test completed") diff --git a/tools/test_server.glm b/tools/test_server.glm new file mode 100644 index 000000000..3ded36ea7 --- /dev/null +++ b/tools/test_server.glm @@ -0,0 +1,5 @@ +#ifexist ../test_server.glm +#define DIR=.. +#endif + +#system python3 ${DIR:-.}/../server.py