Compare commits

..

11 Commits

Author SHA1 Message Date
DBras f5dfda57b7 question: D3 Part 2 Q2 2024-06-10 15:48:18 +02:00
DBras e6c47bac4e dev: plotting file without fluff 2024-06-10 14:54:10 +02:00
DBras d1e6cc19f6 file: setpoints for load test 2024-06-10 14:37:03 +02:00
DBras bb6ebbc9db question: D3 Part 2 2024-06-10 14:34:57 +02:00
DBras fa6eda5c16 question: D3 Q4 2024-06-10 14:15:17 +02:00
DBras 563be46da6 question: D3 Q3 2024-06-10 14:00:46 +02:00
DBras b10d5f623e question: D3 Q2 2024-06-10 13:49:08 +02:00
DBras 3207c3f51f question: D3 Q1 2024-06-10 11:45:01 +02:00
DBras 2a6fc39dc0 question: prep file for answers 2024-06-10 11:33:35 +02:00
DBras a200ff729a fix: env & paths 2024-06-10 11:01:28 +02:00
DBras 1d8fbd49d1 fix: syslab-lib regex escape 2024-06-10 10:51:35 +02:00
84 changed files with 10134 additions and 809 deletions

BIN
2024_config_D3_Demo.pdf Normal file

Binary file not shown.

View File

@ -1,38 +0,0 @@
# Communication pattern exercises
These scripts were developed for the courses 31380 and 31725 by
- Lasse Orda (Initial implementation)
- Tue Vissing Jensen (Updates, maintainer)
## Included scripts
### RPC
`rpc_example_runner.py text1 text2 text3 text4` ---
Requires that `rpc/rpc_example_server.py` is running.
Returns a the list of text strings given, but in reversed order.
`rpc_sync_pi_runner.py N` ---
Requires that `rpc/rpc_pi_server.py` is running.
Estimate pi by throwing N points in the unit circle, with the server taking over half the work.
`rpc_async_pi_runner.py N` ---
Requires that `rpc/rpc_pi_server.py` is running.
Estimate pi by throwing N points in the unit circle, with the server taking over half the work simultaneously.
### Pub/Sub
`pub_server.py` ---
A server which periodically publishes the current time.
`sub_client.py` ---
Subscribes to the server's messages and prints them. Exits after 5 messages.
### Broadcast
`broadcast_receiver.py`
`broadcast_listener.py`
`broadcaster.py` ---
Periodically broadcasts

View File

@ -1,43 +0,0 @@
"""
Script which listens for messages on a hardcoded port 8881
@Author: orda
"""
import socket
import sys
from parse import parse
def main():
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
else:
port = 8881
my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
my_socket.bind(('', port))
formatting_string = "Today's lottery number: {number}"
print('listener started...')
try:
while True:
message, address = my_socket.recvfrom(port)
dmessage = message.decode('utf-8') # Decode to utf-8
print(f'received: {dmessage}, from: {address}')
# decoded will be a pair of a tuple and a dictionary which reflect the
# "reverse" of using .format on the first string.
decoded = parse(formatting_string, dmessage)
if decoded:
print(f' Decoded into: {decoded.named}')
print(f' Check that the string matches: {formatting_string.format(*decoded.fixed, **decoded.named)}')
finally:
my_socket.close()
if __name__ == "__main__":
main()

View File

@ -1,38 +0,0 @@
"""
Script which broadcasts random integers to a hardcoded port 8881
@Author: orda
"""
import socket
import random
import time
import sys
def main():
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
else:
port = 8881
my_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
# Allow reuse in case we exited ungracefully
my_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print('broadcaster started...')
try:
while True:
# Make a random number and send it out.
number = random.randint(1, 101)
print("sending: ", number)
my_socket.sendto(f"Today's lottery number: {number}".encode('utf-8'), ('<broadcast>', 8881))
time.sleep(1)
finally:
my_socket.close()
if __name__ == "__main__":
main()

View File

@ -1,17 +0,0 @@
import zmq
ip = "localhost"
heartbeat_publish_port = 10002
# Get a context we can use to make sockets
context = zmq.Context()
# Socket to talk to server
socket = context.socket(zmq.SUB)
socket.connect(f"tcp://{ip}:{heartbeat_publish_port}")
topicfilter = "HEARTBEAT"
socket.setsockopt_string(zmq.SUBSCRIBE, topicfilter)
while True:
print(socket.recv_string())

View File

@ -1,31 +0,0 @@
import zmq
from xmlrpc.server import SimpleXMLRPCServer
# Other connect on the server port.
heartbeat_server_port = 10001
heartbeat_publish_port = 10002
# Make a context which we use to make sockets from
context = zmq.Context()
# Make a new socket. We want to publish on this socket.
socket = context.socket(zmq.PUB)
# Bind the socket (inside our program) to a port (on our machine)
# We can now send messages
socket.bind(f"tcp://*:{heartbeat_publish_port}")
# Make a function for others to call letting us know they are alive.
def send_heartbeat(sender: str):
print(f"Received heartbeat from {sender}")
# Publish who is alive now
socket.send_string(f"HEARTBEAT;{sender}")
# Return something just to show we succeeded
return 0
# Make an RPC server to serve that function to others
server = SimpleXMLRPCServer(('localhost', heartbeat_server_port))
# Register the function
server.register_function(send_heartbeat, 'send_heartbeat')
# Start up the server
server.serve_forever()

View File

@ -1,11 +0,0 @@
from xmlrpc.client import ServerProxy
from time import sleep
heartbeat_server_port = 10001
with ServerProxy(f'http://localhost:{heartbeat_server_port}') as proxy:
# Periodically send a heartbeat
while True:
proxy.send_heartbeat('BATTERY')
sleep(1)

View File

@ -0,0 +1 @@
Makes folder visible for git.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
Makes folder visible for git.

View File

@ -0,0 +1,4 @@
{"unit": "dumpload_sp", "value": 0.0, "time": 1718013928.6948164}
{"unit": "dumpload_sp", "value": 20.0, "time": 1718013959.6977577}
{"unit": "dumpload_sp", "value": 10.0, "time": 1718013989.6989655}
{"unit": "dumpload_sp", "value": 0.0, "time": 1718014018.716735}

48
demo_datalogger.py Executable file
View File

@ -0,0 +1,48 @@
#!./venv/bin/python3
import syslab
from json import dump
from time import sleep, time
# Defines location and name of the measurements file
LOG_FILE = f'data/measurements/measurements_{time():.00f}.json'
print(f"Logging to file {LOG_FILE}")
# Set up a connection to the switchboards
sb3192 = syslab.SwitchBoard('319-2')
sb3193 = syslab.SwitchBoard('319-3')
sb33012 = syslab.SwitchBoard("330-12")
sb1172 = syslab.SwitchBoard('117-2')
# Convenience function to
def take_measurements():
measurements = {
"pcc_p": sb3192.getActivePower('Grid'),
"pcc_q": sb3192.getReactivePower('Grid'),
"pv319_p": sb3192.getActivePower('PV'),
"pv319_q": sb3192.getReactivePower('PV'),
"dumpload_p": sb3192.getActivePower('Dumpload'),
"dumpload_q": sb3192.getReactivePower('Dumpload'),
"gaia_p": sb33012.getActivePower('Gaia'),
"gaia_q": sb33012.getReactivePower('Gaia'),
"pv330_p": sb33012.getActivePower('PV_1'),
"pv330_q": sb33012.getReactivePower('PV_1'),
"b2b_p": sb3193.getActivePower('ABB_Sec'),
"b2b_q": sb3193.getReactivePower('ABB_Sec'),
"battery_p": sb1172.getActivePower('Battery'),
"battery_q": sb1172.getReactivePower('Battery'),
}
return [{'unit': k, 'value': meas.value, 'time': meas.timestampMicros/1e6} for k, meas in measurements.items()]
while True:
measurement = take_measurements()
# Open the output file in "append" mode which adds lines to the end
with open(LOG_FILE, 'a') as file:
for m in measurement:
# Convert the dictionary m to a json string and put it
# in the file.
dump(m, file)
# Write a newline for each measurement to make loading easier
file.write('\n')
sleep(1)

73
demo_plotter.py Executable file
View File

@ -0,0 +1,73 @@
#!./venv/bin/python3
import pandas as pd
import numpy as np
import json
import matplotlib.pyplot as plt
import os
from datetime import timedelta
## Read the measurements data file ##
DATA_MEAS_DIR = 'data/measurements'
# Always plot latest datafile - replace [-1] with another index if you want to plot a specific file.
MEAS_LOG_FILE = sorted(os.listdir(DATA_MEAS_DIR))[-1]
# Store each dictionary of the measurements json in a list
with open(os.path.join(DATA_MEAS_DIR, MEAS_LOG_FILE)) as f:
meas_data = [json.loads(line) for line in f]
# Use setpoint logger (only necessary for part two of the exercise "collecting fresh data")
use_setpoint_log = False
## Read the setpoints data file ##
if use_setpoint_log:
DATA_SP_DIR = 'data/setpoints'
# Always plot latest datafile
SP_LOG_FILE = sorted(os.listdir(DATA_SP_DIR))[-1]
# Store each dictionary of the setpoints json in a list
with open(os.path.join(DATA_SP_DIR, SP_LOG_FILE)) as f:
sp_data = [json.loads(line) for line in f]
# Merge measurements and setpoints in one list
data = meas_data + sp_data
else:
data = meas_data
################################################################################
################## Part 2 ######################################################
################################################################################
def overshoot(df, T1, T2):
yT1, yT2 = df[T1], df[T2]
over = 1 / (yT1 - yT2) * np.max(yT2 - df)
return over
SETPOINT_UNIX = 1718013959.6977577
SETPOINT_TS = pd.to_datetime(SETPOINT_UNIX, unit='s')
WINDOW = pd.to_datetime(SETPOINT_UNIX+25, unit='s')
## The controller is reasonably fast at reacting to changes; the sum of in and
## out is at zero roughly 5-10 seconds after a change.
# Construct a dataframe and pivot it to obtain a dataframe with a column per unit, and a row per timestamp.
df = pd.DataFrame.from_records(data)
df['time'] = pd.to_datetime(df['time'], unit='s')
df_pivot = df.pivot_table(values='value', columns='unit', index='time')
df_resampled = df_pivot.resample('0.1s').mean()
df_resampled.interpolate(method='linear', inplace=True)
df_resampled = pd.DataFrame(df_resampled)
# Plot the data. Note, that the data will mostly not be plotted with lines.
plt.ion() # Turn interactive mode on
plt.figure()
ax1, ax2 = plt.subplot(211), plt.subplot(212)
df_resampled[[c for c in df_resampled.columns if '_p' in c]].plot(marker='.', ax=ax1, linewidth=3)
ax2.plot(df_resampled['pcc_p'][SETPOINT_TS:WINDOW], marker='.', linewidth=3, label='pcc_p')
ax2.plot(df_resampled['dumpload_p'][SETPOINT_TS:WINDOW], marker='.', linewidth=3, label='dumpload')
plt.legend()
# print(overshoot(df_resampled['pcc_p'][SETPOINT_TS:WINDOW], SETPOINT_TS, WINDOW))
plt.show(block=True)

28
plotter.py Executable file
View File

@ -0,0 +1,28 @@
#!./venv/bin/python3
import pandas as pd
import json
import matplotlib.pyplot as plt
import os
DATA_MEAS_DIR = 'data/measurements'
SPECIFIC_FILE = ''
MEAS_LOG_FILE = sorted(os.listdir(DATA_MEAS_DIR))[-1] if not SPECIFIC_FILE else SPECIFIC_FILE
with open(os.path.join(DATA_MEAS_DIR, MEAS_LOG_FILE)) as f:
meas_data = [json.loads(line) for line in f]
data = meas_data
df = pd.DataFrame.from_records(data)
df['time'] = pd.to_datetime(df['time'], unit='s')
df_pivot = df.pivot_table(values='value', columns='unit', index='time')
df_resampled = df_pivot.resample('s').mean()
df_resampled.interpolate(method='linear', inplace=True)
df_resampled = pd.DataFrame(df_resampled)
# Plot the data. Note, that the data will mostly not be plotted with lines.
plt.ion() # Turn interactive mode on
plt.figure()
ax1, ax2 = plt.subplot(211), plt.subplot(212)
df_resampled[[c for c in df_resampled.columns if '_p' in c]].plot(marker='.', ax=ax1, linewidth=3)
df_resampled[[c for c in df_resampled.columns if '_q' in c]].plot(marker='.', ax=ax2, linewidth=3)
plt.show(block=True)

View File

@ -1,32 +0,0 @@
import zmq
import sys
import time
import random
port = "5556"
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
# Make a context which we use to make sockets from
context = zmq.Context()
# Make a new socket. We want to publish on this socket.
socket = context.socket(zmq.PUB)
# Bind the socket (inside our program) to a port (on our machine)
# We can now send messages
socket.bind(f"tcp://*:{port}")
topics = ('TIME', 'RANDOM')
messages = {}
while True:
# Time to publish the latest time!
messages['TIME'] = time.ctime()
messages['RANDOM'] = random.randint(1,10)
# Note the use of XXX_string here;
# the non-_stringy methods only work with bytes.
for topic in topics:
message = messages.get(topic, '')
if not message: continue
socket.send_string(f"{topic};{message}")
print(f"Published topic {topic}: {message}")
time.sleep(1)

View File

@ -1,47 +0,0 @@
import zmq
import sys
import time
from zmq.utils.monitor import recv_monitor_message
port = "5556"
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind(f"tcp://*:{port}")
# Get a monitoring socket where we can sniff information about new subscribers.
monitor = socket.get_monitor_socket()
sub_list = set()
topic = "TIME"
while True:
# Run through monitoring messages and check if we have new subscribers
# Note you can delete this entire
while True:
try:
# We include a NOBLOCK flag here to not hang until a status message comes in.
# If no messages are ready, zmq.Again will be raised, which we catch below.
status = recv_monitor_message(monitor, flags=zmq.NOBLOCK)
print(f"Status: {status}")
if status['event'] == zmq.EVENT_ACCEPTED:
# New subscriber, add them to our list of subscribers.
print(f"Subscriber '{status['value']}' has joined :D")
sub_list.add(status['value'])
if status['event'] == zmq.EVENT_DISCONNECTED:
# Someone left, remove them from our list.
print(f"Subscriber '{status['value']}' has left :(")
sub_list.remove(status['value'])
except zmq.Again as e:
# No more new subscribers - let's stop looking for them
break
# Time to publish the latest time!
messagedata = time.ctime()
# Note the use of XXX_string here;
# the non-_string-y methods only work with bytes.
socket.send_string(f"{topic};{messagedata}")
print(f"Published topic {topic}: {messagedata} to subscribers: {sub_list}")
time.sleep(1)

View File

@ -1,31 +0,0 @@
import sys
import zmq
ip = "localhost"
port = "5556"
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
# Socket to talk to server
context = zmq.Context()
socket = context.socket(zmq.SUB)
print(f"Collecting updates from time server at tcp://localhost:{port}")
socket.connect(f"tcp://{ip}:{port}")
# Filter by topic
topicfilters = ("TIME", "RANDOM")
socket.setsockopt_string(zmq.SUBSCRIBE, topicfilters[0])
socket.setsockopt_string(zmq.SUBSCRIBE, topicfilters[1])
# Process 5 updates
topic_list = []
for update_nbr in range(5):
string = socket.recv_string()
topic, messagedata = string.split(';')
topic_list.append(messagedata)
print(f"Received on topic {topic}: {messagedata}")
socket.close()
t_str = "\n".join(topic_list)
print(f"All the times we received: \n{t_str}")

View File

@ -1,50 +0,0 @@
import sys
import zmq
from time import sleep
"""
This version of the subscriber doesn't hang
"""
ip = "localhost"
port = "5556"
if len(sys.argv) > 1:
port = sys.argv[1]
int(port)
# Make a context we can use to get a socket
context = zmq.Context()
# Grab a socket that can connect to the server
socket = context.socket(zmq.SUB)
# Connect to the server by saying where we can get our measurements from
print(f"Collecting updates from time server at tcp://localhost:{port}")
socket.connect(f"tcp://{ip}:{port}")
# Filter by topic - we are only interested in knowing about the time
topicfilter = "TIME"
socket.setsockopt_string(zmq.SUBSCRIBE, topicfilter)
# Process 5 updates
topic_list = []
# We need to keep track of how many we've received
# since the loop may end with no messages
try:
while len(topic_list) < 5:
try: # Need to use a try block since zmq uses a
# giving the flag NOBLOCK indicates to zmq that we don't want to
# hang waiting for a message.
string = socket.recv_string(zmq.NOBLOCK)
except zmq.Again:
print("No message this time :(")
continue
topic, messagedata = string.split(';')
topic_list.append(messagedata)
print(f"Received on topic {topic}: {messagedata}")
#
finally
sleep(0.2)
socket.close()
t_str = "\n".join(topic_list)
print(f"All the times we received: \n{t_str}")

View File

@ -1,12 +1,12 @@
aiohttp==3.9.5
aiosignal==1.3.1
attrs==23.2.0
frozenlist==1.4.1
beautifulsoup4==4.12.3
certifi==2024.6.2
charset-normalizer==3.3.2
greenlet==3.0.3
idna==3.7
msgpack==1.0.8
multidict==6.0.5
parse==1.20.2
pynvim==0.5.0
pyzmq==26.0.3
yarl==1.9.4
requests==2.32.3
soupsieve==2.5
# Editable Git install with no remote (syslab==0.3.0)
-e /home/daniel/Dropbox/DTU/F24/46045/syslab/syslab-python
urllib3==2.2.1

Binary file not shown.

View File

@ -1,186 +0,0 @@
"""
XML-RPC Client with asyncio.
This module adapt the ``xmlrpc.client`` module of the standard library to
work with asyncio.
"""
import sys
import asyncio
import logging
import aiohttp
import inspect
import functools
from xmlrpc import client as xmlrpc
def coroutine(fn):
if inspect.iscoroutinefunction(fn):
return fn
@functools.wraps(fn)
async def _wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return _wrapper
__ALL__ = ['ServerProxy', 'Fault', 'ProtocolError']
# you don't have to import xmlrpc.client from your code
Fault = xmlrpc.Fault
ProtocolError = xmlrpc.ProtocolError
log = logging.getLogger(__name__)
PY35 = sys.version_info >= (3, 5)
class _Method:
# some magic to bind an XML-RPC method to an RPC server.
# supports "nested" methods (e.g. examples.getStateName)
def __init__(self, send, name):
self.__send = send
self.__name = name
def __getattr__(self, name):
return _Method(self.__send, "%s.%s" % (self.__name, name))
@coroutine
def __call__(self, *args):
ret = yield from self.__send(self.__name, args)
return ret
class AioTransport(xmlrpc.Transport):
"""
``xmlrpc.Transport`` subclass for asyncio support
"""
def __init__(self, session, use_https, *, use_datetime=False,
use_builtin_types=False, loop, headers=None, auth=None):
super().__init__(use_datetime, use_builtin_types)
self.use_https = use_https
self._loop = loop
self._session = session
self.auth = auth
if not headers:
headers = {'User-Agent': 'python/aioxmlrpc',
'Accept': 'text/xml',
'Content-Type': 'text/xml'}
self.headers = headers
@coroutine
def request(self, host, handler, request_body, verbose=False):
"""
Send the XML-RPC request, return the response.
This method is a coroutine.
"""
url = self._build_url(host, handler)
response = None
try:
response = yield from self._session.request(
'POST', url, headers=self.headers, data=request_body, auth=self.auth)
body = yield from response.text()
if response.status != 200:
raise ProtocolError(url, response.status,
body, response.headers)
except asyncio.CancelledError:
raise
except ProtocolError:
raise
except Exception as exc:
log.error('Unexpected error', exc_info=True)
if response is not None:
errcode = response.status
headers = response.headers
else:
errcode = 0
headers = {}
raise ProtocolError(url, errcode, str(exc), headers)
return self.parse_response(body)
def parse_response(self, body):
"""
Parse the xmlrpc response.
"""
p, u = self.getparser()
p.feed(body)
p.close()
return u.close()
def _build_url(self, host, handler):
"""
Build a url for our request based on the host, handler and use_http
property
"""
scheme = 'https' if self.use_https else 'http'
return '%s://%s%s' % (scheme, host, handler)
class ServerProxy(xmlrpc.ServerProxy):
"""
``xmlrpc.ServerProxy`` subclass for asyncio support
"""
def __init__(self, uri, session=None, encoding=None, verbose=False,
allow_none=False, use_datetime=False, use_builtin_types=False,
loop=None, auth=None, headers=None):
self._loop = loop or asyncio.get_event_loop()
if session:
self._session = session
self._close_session = False
else:
self._close_session = True
self._session = aiohttp.ClientSession(loop=self._loop)
transport = AioTransport(use_https=uri.startswith('https://'),
loop=self._loop,
session=self._session,
auth=auth,
headers=headers)
super().__init__(uri, transport, encoding, verbose, allow_none,
use_datetime, use_builtin_types)
@coroutine
def __request(self, methodname, params):
# call a method on the remote server
request = xmlrpc.dumps(params, methodname, encoding=self.__encoding,
allow_none=self.__allow_none).encode(self.__encoding)
response = yield from self.__transport.request(
self.__host,
self.__handler,
request,
verbose=self.__verbose
)
if len(response) == 1:
response = response[0]
return response
@coroutine
def close(self):
if self._close_session:
yield from self._session.close()
def __getattr__(self, name):
return _Method(self.__request, name)
if PY35:
@coroutine
def __aenter__(self):
return self
@coroutine
def __aexit__(self, exc_type, exc_val, exc_tb):
if self._close_session:
yield from self._session.close()

View File

@ -1,17 +0,0 @@
from xmlrpc.client import ServerProxy
import sys
# Create the proxy in a nice way so it gets closed when we are done.
with ServerProxy('http://localhost:9000') as proxy:
# Ensure we got enough arguments coming in
assert len(sys.argv) >= 3, "Must supply at least 2 arguments.\n" + \
"Usage: rpc_sync_client.py function argument1 [argument2 ...]"
# Split incoming arguments into the name of the function to call and
# the arguments to supply to that function. Note that sys.argv[0] is
# the name of the script itself.
scriptname, function, *arguments = sys.argv
# Get the indicated remote function.
remote_function = getattr(proxy, function)
# Print the result of executing the remote function.
print(remote_function(arguments))

View File

@ -1,26 +0,0 @@
import logging
import time
from xmlrpc.server import SimpleXMLRPCServer
def reverse_list(l):
logging.debug(f'Call received: reverse_list({l!r}), calculating for 1 second')
time.sleep(1)
return l[::-1]
def allcaps_list(l):
logging.debug(f'Call received: allcaps({l!r}), calculating for 1 second')
return [i.upper() for i in l]
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)
# Register the function we are serving
server.register_function(reverse_list, 'reverse')
server.register_function(allcaps_list, 'allcaps')
try:
print("Use Control-C to exit")
# Start serving our functions
server.serve_forever()
except KeyboardInterrupt:
print("Exiting")

View File

@ -1,58 +0,0 @@
import asyncio
import sys
from statistics import mean
from math import pi
from time import time
# Import the asynchronous version of ServerProxy
from aioxmlrpc.client import ServerProxy
# Import the asynchronous version of estimate_pi
from util import estimate_pi_async
# Asynchronous call to the slave
async def remote_estimate(n):
print(f"Requesting that slave estimate pi with {n} throws.")
# Create the proxy in a nice way so it gets closed when we are done.
async with ServerProxy('http://localhost:9000') as proxy:
pi_remote = await proxy.estimate_pi(n)
# print(f"Result of remote estimation: pi={pi_remote:.010f}")
print(pi_remote)
return pi_remote
# Asynchronous call to ourselves
async def local_estimate(n):
print(f"Master begins estimating pi with {n} throws.")
pi_local = await estimate_pi_async(n)
print(f"Result of local estimation: pi={pi_local:.010f}")
return pi_local
if __name__ == "__main__":
# Ensure we got enough arguments coming in
assert len(sys.argv) >= 2, "Must supply at least 1 argument.\n" + \
"Usage: rpc_sync_pi_master.py N [argument2 ...]"
# Split incoming arguments into the number of throws to use.
# Note that sys.argv[0] is the name of the script itself.
scriptname, N, *arguments = sys.argv
# split the workload between ourselves and the remote
# note: // is integer division
N = int(N)
N_remote = N // 2
N_local = N - N_remote
start_time = time()
# ASYNC MAGIC BEGIN
# Gather up all tasks we have to do, and tell the event loop to
# run until they are complete.
futures = asyncio.gather(remote_estimate(N_remote), local_estimate(N_local))
loop = asyncio.get_event_loop()
results = loop.run_until_complete(futures)
# ASYNC MAGIC END
pi_remote, pi_local = results
pi_m = mean([pi_remote, pi_local])
print(f"Mean estimation result: pi ={pi_m:.010f}")
print(f"Relative error: {100*(pi_m/pi - 1):.010f}%")
print(f"Total time to execute: {time() - start_time} sec")

View File

@ -1,21 +0,0 @@
import logging
from xmlrpc.server import SimpleXMLRPCServer
from util import estimate_pi
def local_estimate_pi(n, *args):
logging.debug(f'Call received: estimate_pi({n!r})')
return estimate_pi(n)
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG)
server = SimpleXMLRPCServer(('localhost', 9000), logRequests=True)
# Register the function we are serving
server.register_function(local_estimate_pi, 'estimate_pi')
try:
print("Use Control-C to exit")
# Start serving our functions
server.serve_forever()
except KeyboardInterrupt:
print("Exiting")

View File

@ -1,35 +0,0 @@
import sys
from util import estimate_pi
from statistics import mean
from math import pi
from time import time
# Import the synchronous version of ServerProxy
from xmlrpc.client import ServerProxy
# Create the proxy in a nice way so it gets closed when we are done.
with ServerProxy('http://localhost:9000') as proxy:
# Ensure we got enough arguments coming in
assert len(sys.argv) >= 2, "Must supply at least 1 argument.\n" + \
"Usage: rpc_sync_pi_master.py N [argument2 ...]"
# Split incoming arguments into the number of throws to use.
# Note that sys.argv[0] is the name of the script itself.
scriptname, N, *arguments = sys.argv
# split the workload between ourselves and the remote
# note: // is integer division
N = int(N)
N_remote = N // 2
N_local = N - N_remote
start_time = time()
print(f"Requesting that slave estimate pi with {N_remote} throws.")
pi_remote = proxy.estimate_pi(N_remote)
print(f"Result of remote estimation: pi={pi_remote:.010f}")
print(f"Master begins estimating pi with {N_local} throws.")
pi_local = estimate_pi(N_local)
print(f"Result of local estimation: pi={pi_local:.010f}")
pi_m = mean([pi_remote, pi_local])
print(f"Mean estimation result: pi ={pi_m:.010f}")
print(f"Relative error: {100*(pi_m/pi - 1):.010f}%")
print(f"Total time to execute: {time() - start_time} sec")

View File

@ -1,55 +0,0 @@
import asyncio
from random import random
from itertools import chain, islice
def get_chunks_it(l, n):
""" Chunks an iterator `l` in size `n`
Args:
l (Iterator[Any]): an iterator
n (int): size of
Returns:
Generator[Any]
"""
iterator = iter(l)
for first in iterator:
yield chain([first], islice(iterator, n - 1))
def estimate_pi(n):
"""
Estimates pi by throwing a point (x,y) randomly *n* times in
the unit square and counting the number of hits where
x^2 + Y^2 <= 1.
Pi is then approximated as 4 * no. hits / n.
input:
*n* (int): The number of times to throw the point
output:
*estimate* (float): The estimate for pi found here
"""
hits = sum(int(random()**2 + random()**2 <= 1) for _ in range(n))
estimate = 4 * hits / n
return estimate
async def estimate_pi_async(n):
"""
Estimates pi by throwing a point (x,y) randomly *n* times in
the unit square and counting the number of hits where
x^2 + Y^2 <= 1.
Pi is then approximated as 4 * no. hits / n.
input:
*n* (int): The number of times to throw the point
output:
*estimate* (float): The estimate for pi found here
**Note:**
This is an asynchronous implementation that throws
100 points before awaiting to relinquish control.
"""
hits = 0
for chunk in get_chunks_it(range(n), 100):
await asyncio.sleep(0) # Relinquish control so something else can run
hits += sum(int(random()**2 + random()**2 <= 1) for _ in chunk)
estimate = 4 * hits / n
return estimate

109
syslab-python/.gitignore vendored Normal file
View File

@ -0,0 +1,109 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
\.DS_Store
# Pycharm
.idea/

27
syslab-python/README.md Normal file
View File

@ -0,0 +1,27 @@
# SYSLAB Python Interface
This project provides a Python interface to several SYSLAB components.
## Required software
- Python (>=3.7)
- python-requests (>=2.18)
- python-bs4 (>= 4.7)
## Installation
To install the package in your local path, run
```shell
python setup.py install
```
Alternatively, copy the `syslab` folder into your project directory.
# Contributors
- Anders Thavlov: Initial implementation
- Oliver Gehrke
- Daniel Esteban Morales Bondy
- Tue Vissing Jensen
- Federico Zarelli

11
syslab-python/example.py Normal file
View File

@ -0,0 +1,11 @@
from syslab import SwitchBoard
name = '319-2'
SB_connection = SwitchBoard(name)
print("Let's look at what is going on in the switchboard {}.".format(name))
for bay in range(SB_connection.getNumBays()):
print(SB_connection.getBayName(bay),' : ',SB_connection.getActivePower(bay))

99
syslab-python/notes.md Normal file
View File

@ -0,0 +1,99 @@
# Notes on SOAP implementation
- Units should reflect SOAP methods onto their own namespace (1)
- CompositeMeasurement should convert to/from SOAP
- Type checking via client.get\_type('ns0:compositeMeasurement').elements
-
# Notes about this module - to be discussed
SYSLAB\_Unit.py has a whole bunch of static methods - should these be split into a util library instead?
Generally, many places where static methods are used for things that should perhaps just be functions...
The following files are empty:
- BattOpMode.py
- FlowBatteryState.py
- GaiaWindTurbine.py
To check the methods available, use, e.g.:
http://syslab-33.syslab.dk:8080/typebased_WebService_HeatSubstation/HeatSwitchboardWebService/716-h1/resourceNames
To figure out this URL, look at software.xml for the corresponding machine, and use this template:
http://(machineName).syslab.dk:(port)/(interfaceName)/(shortServerName)/(unitname)/resourceNames
| field | corresponds to | notes |
| ----- | -------------- | ----- |
| machineName | N/A | Look this up on the wiki |
| port | N/A | Dynamically allocated, starting at 8080 - good luck! |
| interfaceName | typeBasedWebService, interfaceName | |
| shortServerName | typeBasedWebService, serverClass | Remove the "Server" at the end |
| unitname | dataLogger, unit | Also defined as "name" in hardware.xml |
-------------------------
SYSLAB COMMON
Broadcast event logger:
Transcode to python:
https://git.elektro.dtu.dk/syslab/syslab-common/-/blob/master/src/main/java/risoe/syslab/comm/broadcast/BroadcastLogSender.java
broadcast log sender
:: transcode the "send()" method to python. It byte-encodes the message for UDP.
Java:
send(String origin, byte[] origIP, long timestamp, int ploadType, String message,
int level, int flags, String[] tags)
------------
Python:
----------.
def send(origin, origIP, timestamp, ploadType, message, level, flags, tags):
ploadbytes = message[:min(1024, len(message))].encode()
origbytes = origin[:min(32, len(origin))].encode()
tagbytes = tagsToBytes(tags, 256)
pktlen = 2 + 2 + 1 + len(origbytes) + 4 + 2 + 2 + 8 + 1 + 2 + len(ploadbytes) + len(tagbytes)
buf = bytearray(pktlen)
buf[0] = BroadcastLogConstants.BROADCASTLOG_PKTID >> 8
buf[1] = BroadcastLogConstants.BROADCASTLOG_PKTID & 0xff
buf[2] = (pktlen >> 8) & 0xff
buf[3] = pktlen & 0xff
buf[4] = len(origbytes)
buf[5:5+len(origbytes)] = origbytes
writePtr = 5 + len(origbytes)
buf[writePtr:writePtr+4] = origIP
writePtr += 4
buf[writePtr] = (level >> 8) & 0xff
buf[writePtr+1] = level & 0xff
buf[writePtr+2] = (flags >> 8) & 0xff
buf[writePtr+3] = flags & 0xff
for i in range(8):
buf[writePtr+7-i] = timestamp & 0xff
timestamp >>= 8
writePtr += 8
buf[writePtr] = ploadType & 0xff
buf[writePtr+1] = (len(ploadbytes) >> 8) & 0xff
buf[writePtr+2] = len(ploadbytes) & 0xff
buf[writePtr+3:writePtr+3+len(ploadbytes)] = ploadbytes
writePtr += len(ploadbytes)
buf[writePtr:writePtr+len(tagbytes)] = tagbytes
pack = n
pack = DatagramPacket(buf, len(buf), InetAddress.getByName("localhost"), 4445)
sock.send(pack)
------------
broadcast log receiver
+ needs a logger
listener ist interface for receiver
gui wall (SYSLAB Userspacce)
broadcast log displet
https://git.elektro.dtu.dk/syslab/syslab-userspace/-/blob/master/src/main/java/risoe/syslab/gui/wall/displets/BroadcastLogDisplet.java
... maybe extend with simple log file writer.

View File

@ -0,0 +1,2 @@
requests >= 2.18
beautifulsoup4 >= 3.7

10
syslab-python/setup.cfg Normal file
View File

@ -0,0 +1,10 @@
[flake8]
ignore =
max-line-length = 79
max-complexity = 11
[pytest]
addopts = --doctest-glob="*.rst"
[wheel]
universal = True

36
syslab-python/setup.py Normal file
View File

@ -0,0 +1,36 @@
from setuptools import setup, find_packages
setup(
name='syslab',
version='0.3.0',
author='Tue Vissing Jensen',
author_email='tvjens at elektro.dtu.dk',
description=('SYSLAB webservice client library.'),
long_description=(''),
url='https://www.syslab.dk',
install_requires=[
'requests>=2.18',
'beautifulsoup4>=3.7',
],
packages=find_packages(exclude=['tests*']),
include_package_data=True,
entry_points={
'console_scripts': [
],
},
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Console',
'Intended Audience :: Science/Research',
'License :: Other/Proprietary License',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Topic :: Scientific/Engineering',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
SYSLAB library
~~~~~~~~~~~~~~~~~~~~~
SYSLAB is a Python 3 library for control and monitoring of units in SYSLAB. Usage of this library requires the running
computer to be located in the SYSLAB network, either physically or virtually, ie. using a VPN connection.
"""
__title__ = 'syslab'
__version__ = '0.3.0'
__author__ = 'Anders Thavlov, Daniel Esteban Morales Bondy, Tue Vissing Jensen'
__maintainer__ = 'Tue Vissing Jensen'
__license__ = ''
__copyright__ = 'DTU'
# Raise error if we are not on Python 3
import sys
if not (sys.version_info.major == 3 and sys.version_info.minor >= 5):
raise RuntimeError("Must be using at least Python 3.5")
# DataTypes
from .core.datatypes import CompositeMeasurement
from .core.datatypes import CompositeBoolean
# Physical units
from .physical import \
SwitchBoard, \
Dumpload, \
Photovoltaics, \
Battery, \
HeatSwitchBoard, \
WindTurbine, \
DieselGenerator, \
B2BConverter, \
MeteoMast, \
EVSE
# Logging utilities
#from .comm.LogUtils import e

View File

@ -0,0 +1,59 @@
###############################################################
# SYSLAB remote logging utilities v0.9
# collect and record log events
#
# Author: Kai Heussen
# Date: 2023/06/18
###############################################################
import logging
import logging.handlers
import time
import syslab.config as config
event_log_formats = '%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(funcName)s - %(message)s'
simple_log_formats = '%(asctime)s - %(name)s - %(message)s'
def setup_event_logger(loggername=config.EVLOG_NAME, host='localhost', port=config.REMOTE_PORT_EV, level=logging.INFO, formats=simple_log_formats): # UDP
handler1 = logging.handlers.DatagramHandler(config.REMOTE_IP_EV, config.REMOTE_PORT_EV)
handler2 = logging.handlers.DatagramHandler(host, port)
handler3 = logging.StreamHandler()
formatter = logging.Formatter(formats)
handler3.setFormatter(formatter)
loggr = logging.getLogger(loggername)
loggr.setLevel(level)
loggr.addHandler(handler1)
loggr.addHandler(handler2)
loggr.addHandler(handler3)
return loggr
def setup_local_logger(loggername=config.EVLOG_NAME, host='localhost', port=config.REMOTE_PORT_EV, level=logging.INFO, formats=simple_log_formats): # UDP
handler = logging.handlers.DatagramHandler(host, port)
formatter = logging.Formatter(formats)
handler.setFormatter(formatter)
loggr = logging.getLogger(loggername)
loggr.setLevel(level)
loggr.addHandler(handler)
return loggr
def setup_udp_logger(loggername=config.SPLOG_NAME, host=config.REMOTE_IP_SP, port=config.REMOTE_PORT_SP, level=logging.INFO, formats=config.LOG_FORMATS): # UDP
handler = logging.handlers.DatagramHandler(host, port)
formatter = logging.Formatter(formats)
handler.setFormatter(formatter)
loggr = logging.getLogger(loggername)
loggr.setLevel(level)
loggr.addHandler(handler)
return loggr
if __name__ == "__main__":
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s %(levelname)-8s %(message)s')
# logging.getLogger().addHandler(logging.handlers.DatagramHandler('10.42.242.3', 51010))
logger = setup_local_logger()
while True:
logger.debug("This shouldn't show up")
logger.info("This should show up")
time.sleep(3)

View File

@ -0,0 +1,2 @@
#from BroadcastLogger import BroadcastLogSender
#from LogUtils import setup_udp_logger

View File

@ -0,0 +1,80 @@
###############################################################
# SYSLAB remote logger v0.9
# utility to display and record log events
# usage: move bash to target log directory, then call
# python <path to syslab-python>/syslab/comm/logrec.py
#
# Author: Kai Heussen
# Date: 2023/06/18
###############################################################
import pickle
import logging
import socket
from syslab import config
# import sys
from time import time
# from json import dump
BASIC = True
FILE = True
FILE_RAW = False # TODO: write a JSON formatter - based on https://stackoverflow.com/questions/50144628/python-logging-into-file-as-a-dictionary-or-json
logtype = "EV"
time=time()
logfile =f"syslab_{logtype}_log_{time:.00f}.txt"
logfile_raw =f"syslab_{logtype}_log_{time:.00f}.raw" # not yet json
DEFAULT_PORT = config.REMOTE_PORT_SP if logtype == "SP" else config.REMOTE_PORT_EV
port = DEFAULT_PORT
#simple_formats = '%(asctime)s - %(name)s - %(message)s'
simple_formats = '%(asctime)s - %(module)s - %(message)s'
#formats='foo: %(levelname)s - %(module)s.%(funcName)s - %(message)s'
event_log_formatter = '%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(module)s - %(funcName)s - %(message)s'
formats = simple_formats
formatter = logging.Formatter(formats)
if BASIC:
logging.basicConfig(format=formats)
else:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
if FILE:
handler = logging.FileHandler(logfile)
handler.setLevel(logging.INFO)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
logger = logging.getLogger()
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('', port))
print(f'Listening for log records on port {port}...')
if FILE:
print(f'recording log entries in file: {logfile}')
if FILE_RAW:
print(f'recording raw log entries in file: {logfile_raw}')
try:
while True:
d, _ = s.recvfrom(1024)
msg = pickle.loads(d[4:])
logrec = logging.makeLogRecord(msg)
logger.handle(logrec)
if FILE_RAW:
with open(logfile_raw, 'a') as file:
# dump(logrec, file) # requires a JSON formatter
file.write(f'{logrec.__str__() }\n') # Write a newline for each measurement to make loading easier
#print(log)
finally:
s.close()
if FILE_RAW:
file.close()

View File

@ -0,0 +1,19 @@
DEBUG = False
SPLOG_NAME = 'SetPointLogger'
EVLOG_NAME = 'EventLog'
REMOTE_LOGGER = True
REMOTE_IP_SP = '10.42.242.3' # UI Machine 03
REMOTE_IP_EV = '10.42.242.3' # UI Machine 03
REMOTE_PORT_SP = 51010
REMOTE_PORT_EV = 51020
LOCAL_IP = 'localhost'
LOCAL_PORT_SP = 51010
LOCAL_PORT_EV = 51020
LOCAL_LOGGER = False
LOG_FORMATS = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' # '%(asctime)s %(levelname)-8s %(message)s'
TEST_LOCAL = False
if TEST_LOCAL:
REMOTE_PORT_SP = LOCAL_PORT_SP
REMOTE_PORT_EV = LOCAL_PORT_EV
REMOTE_IP_SP = LOCAL_IP
REMOTE_IP_EV = LOCAL_IP

View File

@ -0,0 +1,306 @@
from .datatypes import CompositeBoolean, BattOpMode, CompositeMeasurement
from .datatypes import FlowBatteryState, ConvState, ConvOpMode
from .datatypes import HeatCirculationPumpMode, HeatCirculationPumpState
import syslab.config as config
class SyslabUnit:
__HIDDEN_RESOURCES = {
'authenticate': ('boolean', 'String', 'String'),
'isAuthenticated': ('boolean',),
'checkConnection': ('boolean',),
'logout': ('void',)}
def __init__(self, baseurl, units=None, which=None, host=None, port=None, unit_name=None, unit_type=""):
"""
Initialize a proxy to the given unit.
:param baseurl: Example: 'http://{host}:{port}/typebased_WebService_Battery/VRBBatteryWebService/{unit_name}/'
:param units: dictionary of units to be loaded in the format {'which': ('host', 'port', 'unit_name')}
:param which: string indicating which unit we pick from _units_
:param host: (optional) override host
:param port: (optional) override port
:param unit_name: (optional) override unit_name
:param unit_type: (optional) Used to indicate the type of the unit.
"""
if units is None:
units = {}
if which is not None:
if which not in units:
raise TypeError(
'The {unit_type} should be one of: "{list_of_units}"'.format(
unit_type=unit_type,
list_of_units='", "'.join(units.keys())
))
else:
host, port, unit_name = units.get(which)
if host is not None:
host = host
if port is not None:
port = port
assert host is not None, "When assigning custom port, host must not be None."
assert port is not None, "When assigning custom host, port must not be None."
assert unit_name is not None, "When assigning custom host and port, unit_name must not be None."
url = baseurl.format(host=host, port=port, unit_name=unit_name)
print(url)
self._url = url
self._resources = parse_resources(url)
self._resources = {**self._resources, **SyslabUnit.__HIDDEN_RESOURCES}
# TODO: Do type checking on these types. Ignore for now
self._complex_arg_types = ['BattOpMode', 'CompositeMeasurement', 'HeatCirculationPumpMode']
if config.REMOTE_LOGGER:
#import logger
if config.DEBUG:
print(f"Setting up remote logger with default IP: {config.REMOTE_IP} and port: {config.REMOTE_PORT}")
from uuid import uuid1, getnode
from ..comm.LogUtils import setup_udp_logger
self.__logger = setup_udp_logger(config.SPLOG_NAME)
self.__session_id = uuid1().__str__()
self.__host_id = getnode().__str__()
# TODO: Set up local setpoints Logger
if config.LOCAL_LOGGER:
pass
def __request_int(self, resource, method, args, body) -> int:
return int(send_request(self._url, resource, method, args, body))
def __request_float(self, resource, method, args, body) -> float:
return float(send_request(self._url, resource, method, args, body))
def __request_boolean(self, resource, method, args, body) -> bool:
return bool(send_request(self._url, resource, method, args, body))
def __request_string(self, resource, method, args, body) -> str:
return send_request(self._url, resource, method, args, body)
def __request_void(self, resource, method, args, body) -> None:
send_request(self._url, resource, method, args, body)
def __request_composite_boolean(self, resource, method, args, body) -> CompositeBoolean:
if self.__get_resource_return_type(resource) == 'CompositeBoolean':
json_string = self.__request_string(resource, method, args, body)
result = CompositeBoolean.parseFromJSON(json_string)
else:
raise TypeError('Error: resource "{0}" does not return a CompositeBoolean.'.format(resource))
return result
def __request_composite_measurement(self, resource, method, args, body) -> CompositeMeasurement:
if self.__get_resource_return_type(resource) == 'CompositeMeasurement':
json_string = self.__request_string(resource, method, args, body)
result = CompositeMeasurement.parseFromJSON(json_string)
else:
raise TypeError('Error: resource "{0}" does not return a CompositeMeasurement.'.format(resource))
return result
def _request_resource(self, resource, args=(), method='get', body=None, check_types=True):
if not type(resource) is str:
raise TypeError("Resource should be a string, found {0}.".format(type(resource)))
if resource not in self._resources:
# print(resource)
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
if not resource[-1] in ('1', '2'):
raise ValueError('Unknown resource: {0} - no such resource found for {1}'.format(resource, self._url))
if type(args) is int or type(args) is float or type(args) is str:
args = (args,)
if config.DEBUG:
print("Resource: ", resource, " and args: ", args, "with method: ", method)
if config.REMOTE_LOGGER and method == 'put':
logstr = f'SessionID:{self.__session_id}||SenderID:{self.__host_id}||Unit-URL:{self._url}||Setterfcn:{resource}||ArgValue:{args}'
self.__logger.info(logstr)
return_type = self.__get_resource_return_type(resource)
self.__check_argument(resource, args)
result = None
if check_types:
try:
if return_type == 'CompositeMeasurement':
result = self.__request_composite_measurement(resource, method, args, body)
elif return_type == 'CompositeBoolean':
result = self.__request_composite_boolean(resource, method, args, body)
elif return_type == 'String':
result = self.__request_string(resource, method, args, body)
elif return_type == 'int':
result = self.__request_int(resource, method, args, body)
elif return_type == 'boolean':
result = self.__request_boolean(resource, method, args, body)
elif return_type == 'float' or return_type == 'double':
result = self.__request_float(resource, method, args, body)
elif return_type == 'void':
self.__request_string(resource, method, args, body)
elif return_type == 'BattOpMode':
result = BattOpMode.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'FlowBatteryState':
result = FlowBatteryState.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'ConvState':
result = ConvState.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'EVSEState':
result = EVSEState.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'ConvOpMode':
result = ConvOpMode.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'HeatCirculationPumpMode':
result = HeatCirculationPumpMode.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'HeatCirculationPumpState':
result = HeatCirculationPumpState.parseFromJSON(self.__request_string(resource, method, args, body))
elif return_type == 'WYEV' or return_type == 'WYEA' or return_type == 'DELV':
import json
result = json.loads(self.__request_string(resource, method, args, body))
else:
raise TypeError(
'TypeError: The return type {0} by {1} is unknown or not supported yet.'.format(return_type,
resource))
except KeyError as e:
# raise e
raise ValueError('{0} - no such resource found for {1}'.format(resource, self._url))
else:
import json
return_string = self.__request_string(resource, method, args, body)
try:
result = json.loads(return_string)
except RecursionError:
raise RecursionError(
"Maximum recursion depth exceeded while decoding "
"a JSON array from a unicode string. Length: {0}".format(
len(return_string)))
return result
def __get_resource_return_type(self, resource):
if not resource[-1] in ('1', '2'):
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
return self._resources[resource][0]
else:
return self._resources[resource[:-1]][0]
def __get_resource_argument_types(self, resource):
if not resource[-1] in ('1', '2'):
# FIXME: Workaround for metmast, where string-based methods are suffixed by a '2' and int-based methods by a '1'
return self._resources[resource][1:]
else:
return self._resources[resource[:-1]][1:]
def __check_argument(self, resource, args):
arg_types = self.__get_resource_argument_types(resource)
try:
# TODO Ignore complex arguments (for now)
for complex_arg_type in self._complex_arg_types:
if complex_arg_type in arg_types:
return
if len(args) == len(arg_types):
for arg, arg_type in zip(args, arg_types):
if arg_type == 'String' and type(arg) is not str:
raise TypeError()
elif arg_type == 'int' and type(arg) is not int:
raise TypeError()
elif (arg_type == 'float' or arg_type == 'double') and type(arg) is not float and type(
arg) is not int:
raise TypeError()
else:
raise TypeError()
except TypeError:
raise TypeError(
'The resource "{0}" requires exactly {1} argument(s) of type {2}.'.format(resource, len(arg_types),
arg_types))
def parse_resources(url):
import re
from ast import literal_eval
result = {}
resources = send_request(url, 'resourceNames', 'get')
resources = literal_eval(resources)
for resource in resources:
try:
# TODO - handle java arrays, ie, []
m = re.match(r"(\w+)\[?\]? (\w+)(\([\w ,]*\))?", resource).groups()
except AttributeError as e:
print(e)
continue
args = ''
if m[2] is not None:
args = tuple(m[2].replace(' ', '').replace('(', '').replace(')', '').split(','))
result[m[1]] = (m[0],) + args
else:
result[m[1]] = (m[0],)
if config.DEBUG:
print(f'Key: {m[1]} - {m[0]} -- {args}')
return result
def send_request(url, resource, method, args=None, body=None):
from requests import request
from requests.exceptions import ConnectionError, RequestException
if not type(url) is str:
raise TypeError('URL should be a string, found {0}'.format(type(url)))
if not type(resource) is str:
raise TypeError('URL should be a string, found {0}'.format(type(resource)))
kwargs = {}
kwargs.setdefault('allow_redirects', True)
kwargs.setdefault('headers', {'accept': 'application/json', 'content-type': 'application/json'})
if not url.endswith('/'):
url += '/'
full_url = url + resource + '/'
if args is not None and len(args) > 0:
full_url += '/'.join(tuple(str(x) for x in args))
if config.DEBUG:
print(f'Calling: {full_url}')
try:
response = request(method, full_url, data=body, **kwargs)
except ConnectionError as e:
print('Unable to connect to host: {0}.'.format(url))
raise e
except RequestException as e:
print('Request error from host: {0}.'.format(full_url))
raise e
if 200 <= response.status_code < 300:
result = response.text
elif response.status_code == 404:
raise ConnectionError(
'Resource not found on host or arguments are not formatted correctly: {0}'.format(response.text))
elif response.status_code == 405:
raise ConnectionError('Method not allowed on host: \n {0}'.format(response.text))
elif response.status_code == 500:
from pprint import pprint
# TODO: Handle exception
print('Exception on server:')
pprint(response.json())
raise
else:
raise ConnectionError(
'Unable to successfully connect to host. Returned with HTTP status code {0}.\n Content: {1}'.format(
response.status_code, response.text))
if config.DEBUG:
print(f'Succesfully called {full_url}')
print(f'Returned: {result}')
return result

View File

@ -0,0 +1 @@
from . import *

View File

@ -0,0 +1,47 @@
class BattOpMode:
_OPMODES = {
0: 'BATT_UNKNOWN',
1: 'BATT_AUTO',
2: 'BATT_AUTO_SUBMODE1',
3: 'BATT_AUTO_SUBMODE2',
4: 'BATT_AUTO_SUBMODE3',
5: 'BATT_AUTO_SUBMODE4',
6: 'BATT_MANUAL',
7: 'BATT_OFF',
8: 'BATT_NOBMS',
9: 'BATT_WITHBMS', }
def __init__(self, mode):
assert mode in BattOpMode.__OPMODES
self._mode = mode
@property
def mode(self):
return self._mode
def modeAsString(self):
return BattOpMode.__OPMODES[self.mode]
def __str__(self):
return 'BattOpMode: {0} ({1})'.format(self.mode, BattOpMode.__OPMODES[self.mode])
def parseToJSON(self):
from json import dumps
jsonObj = {};
jsonObj['mode'] = self.mode;
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None ## Appears unused
try:
jsonObj = loads(jsonString)
return BattOpMode(jsonObj.get('mode'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,5 @@
# TODO: Implement
class CommonDeviceConfig:
pass

View File

@ -0,0 +1,68 @@
class CompositeBoolean:
def __init__(self, value, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
self._value = value
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision if timePrecision != None else 0
self._quality = quality if quality != None else 0
self._validity = validity if validity != None else 0
self._source = source if source != None else 0
@property
def value(self):
return self._value
@property
def timestampMicros(self):
return self._timestampMicros
@property
def timePrecision(self):
return self._timePrecision
@property
def quality(self):
return self._quality
@property
def validity(self):
return self._validity
@property
def source(self):
return self._source
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._value, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
from datetime import datetime
return 'CompositeBoolean: {0} (@time: {1} UTC) '.format(self.value, datetime.utcfromtimestamp(self.timestampMicros/1000000))
def parseToJSON(self):
from json import dumps
jsonObj = {};
jsonObj['value'] = self.value;
jsonObj['timestampMicros'] = self.timestampMicros;
jsonObj['timePrecision'] = self.timePrecision
jsonObj['quality'] = self.quality
jsonObj['validity'] = self.validity
jsonObj['source'] = self.source;
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
try:
jsonObj = loads(jsonString)
result = CompositeBoolean(jsonObj.get('value'), jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'), jsonObj.get('validity'), jsonObj.get('source'))
return result
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,75 @@
MICROS_PER_SECOND = 1000000
def none_to_zero(x):
if x is None:
return 0
return x
class CompositeMeasurement:
def __init__(self, value, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
self._value = value
self._timestampMicros = timestampMicros
self._timePrecision = none_to_zero(timePrecision)
self._quality = quality if quality != None else 0
self._validity = validity if validity != None else 0
self._source = source if source != None else 0
@property
def value(self):
return self._value
@property
def timestampMicros(self):
return self._timestampMicros
@property
def timePrecision(self):
return self._timePrecision
@property
def quality(self):
return self._quality
@property
def validity(self):
return self._validity
@property
def source(self):
return self._source
def __str__(self):
from datetime import datetime
return 'CompositeMeasurement: {0} (@time: {1})'.format(self.value, datetime.fromtimestamp(self.timestampMicros/MICROS_PER_SECOND))
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._value, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def parseToJSON(self):
from json import dumps
jsonObj = {};
jsonObj['value'] = self.value;
jsonObj['timestampMicros'] = self.timestampMicros;
jsonObj['timePrecision'] = self.timePrecision
jsonObj['quality'] = self.quality
jsonObj['validity'] = self.validity
jsonObj['source'] = self.source;
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
try:
jsonObj = loads(jsonString)
result = CompositeMeasurement(jsonObj.get('value'), jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'), jsonObj.get('validity'), jsonObj.get('source'))
return result
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,5 @@
# TODO: Add Processing here
class CompositeStatus:
pass

View File

@ -0,0 +1,132 @@
from .CompositeStatus import CompositeStatus
class ConvOpMode:
_OPMODES = {
0: 'CONV_UNKNOWN',
1: 'CONV_PQ',
2: 'CONV_UF',
3: 'CONV_OFF',
}
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
assert status in ConvOpMode._OPMODES
self._status = status
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@property
def mode(self) -> int:
return self._status
def modeAsString(self):
return ConvOpMode._OPMODES[self.mode]
def __str__(self):
return f'ConvOpMode({self.mode}; {ConvOpMode._OPMODES[self.mode]})'
def parseToJSON(self):
from json import dumps
jsonObj = {'mode': self.mode}
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str:
raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
try:
jsonObj = loads(jsonString)
return ConvOpMode(**jsonObj.get('mode'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e
class ConvState:
_STATES = {
0: 'CONV_UNKNOWN',
1: 'CONV_STOPPED',
2: 'CONV_STARTING',
4: 'CONV_RUNNING',
8: 'CONV_STOPPING',
16: 'CONV_SYNCED',
32: 'CONV_DROOP',
64: 'CONV_LOADENABLED',
128: 'CONV_INHIBITED',
1024: 'CONV_READY',
2048: 'CONV_WARNING',
4096: 'CONV_ALARM'}
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
self._status = status
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@property
def status(self):
return self._status
@property
def timestampMicros(self):
return self._timestampMicros
@property
def timePrecision(self):
return self._timePrecision
@property
def quality(self):
return self._quality
@property
def validity(self):
return self._validity
@property
def source(self):
return self._source
def statusAsString(self):
return ConvState._STATES[self.status]
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._status, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
from datetime import datetime
return 'FlowBatteryState: {0} : {1} (@time: {2}))'.format(
self._status,
# status & statuscode evaluates to True if statuscode is 2^k and in the binary expansion of status
";".join([label for statuscode, label in ConvState._STATES.items() if self._status & statuscode]),
datetime.fromtimestamp(self.timestampMicros / 1000000))
def parseToJSON(self):
from json import dumps
json_obj = {'status': self._status}
return dumps(json_obj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None
try:
jsonObj = loads(jsonString).get('status')
return ConvState(
jsonObj.get('status'), jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'), jsonObj.get('validity'), jsonObj.get('source'))
except JSONDecodeError as e:
print('Not a valid JSON string in parsing Converter Status: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,83 @@
class EVSEState:
_STATES = {
0: 'EVSE_UNKNOWN',
1: 'EVSE_NO_EV',
2: 'EVSE_EV_STOPPED',
4: 'EVSE_EV_READY',
8: 'EVSE_EV_STARTING',
16: 'EVSE_EV_CHARGING',
32: 'EVSE_EV_DISCHARGING',
64: 'EVSE_EV_PAUSED',
128: 'EVSE_EV_ALARM',
256: 'EVSE_EV_ESD', }
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
self._status = status
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@property
def status(self):
return self._status
@property
def timestampMicros(self):
return self._timestampMicros
@property
def timePrecision(self):
return self._timePrecision
@property
def quality(self):
return self._quality
@property
def validity(self):
return self._validity
@property
def source(self):
return self._source
def statusAsString(self):
return EVSEState.__STATES[self.status]
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._status, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
from datetime import datetime
return 'EVSEState: {0} : {1} (@time: {2}))'.format(
self.status,
# status & statuscode evaluates to True if statuscode is 2^k and in the binary expansion of status
";".join([label for statuscode, label in EVSEState._STATES.items() if self._status & statuscode]),
datetime.fromtimestamp(self.timestampMicros / 1000000))
def parseToJSON(self):
from json import dumps
jsonObj = {'status': self.status}
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None
try:
jsonObj = loads(jsonString).get('status')
return EVSEState(
jsonObj.get('status'), jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'), jsonObj.get('validity'), jsonObj.get('source'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,88 @@
class FlowBatteryState:
_STATES = {
0: 'BATT_UNKNOWN',
1: 'BATT_STOPPED',
2: 'BATT_STARTING',
4: 'BATT_FLOODING',
8: 'BATT_RUNNING',
16: 'BATT_STOPPING',
32: 'BATT_DRAINING',
64: 'BATT_PUMPSRAMP',
128: 'BATT_TANKVALVESOP',
256: 'BATT_PUMPSRUN',
512: 'BATT_DCBREAKER',
1024: 'PCS_READY',
2048: 'PCS_RUNNING',
4096: 'BATT_ALARM',
8192: 'BATT_ESD'}
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
self._status = status
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@property
def status(self):
return self._status
@property
def timestampMicros(self):
return self._timestampMicros
@property
def timePrecision(self):
return self._timePrecision
@property
def quality(self):
return self._quality
@property
def validity(self):
return self._validity
@property
def source(self):
return self._source
def statusAsString(self):
return FlowBatteryState.__STATES[self.status]
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._status, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
from datetime import datetime
return 'FlowBatteryState: {0} : {1} (@time: {2}))'.format(
self.status,
# status & statuscode evaluates to True if statuscode is 2^k and in the binary expansion of status
";".join([label for statuscode, label in FlowBatteryState._STATES.items() if self._status & statuscode]),
datetime.fromtimestamp(self.timestampMicros / 1000000))
def parseToJSON(self):
from json import dumps
jsonObj = {'status': self.status}
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None
try:
jsonObj = loads(jsonString).get('status')
return FlowBatteryState(
jsonObj.get('status'), jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'), jsonObj.get('validity'), jsonObj.get('source'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,89 @@
class HeatCirculationPumpMode:
__OPMODES = {
-1: 'PUMP_MODE_UNKNOWN',
0: 'PUMP_MODE_CONSTANT_SPEED',
1: 'PUMP_MODE_CONSTANT_FREQUENCY',
3: 'PUMP_MODE_CONSTANT_HEAD',
4: 'PUMP_MODE_CONSTANT_PRESSURE',
5: 'PUMP_MODE_CONSTANT_DIFF_PRESSURE',
6: 'PUMP_MODE_PROPORTIONAL_PRESSURE',
7: 'PUMP_MODE_CONSTANT_FLOW',
8: 'PUMP_MODE_CONSTANT_TEMP',
10: 'PUMP_MODE_CONSTANT_LEVEL',
128: 'PUMP_MODE_AUTO_ADAPT',
129: 'PUMP_MODE_FLOW_ADAPT' }
__OPMODES_R = {v: k for k, v in __OPMODES.items()}
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
"""
@Input:
mode (int or str): Pump operating mode, can be both int and string
To get list of available modes: HeatCirculationPumpMode.opmodes()
"""
if status in HeatCirculationPumpMode.__OPMODES:
self._status = status
elif mode in HeatCirculationPumpMode.__OPMODES_R:
self._status = HeatCirculationPumpMode.__OPMODES_R[status]
else:
raise AssertionError('HeatCirculationPumpMode not recognized: \'{0}\''.format(mode))
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@staticmethod
def opmodes():
return HeatCirculationPumpMode.__OPMODES.copy()
@property
def mode(self):
return self._status
@property
def modeAsString(self):
return HeatCirculationPumpMode.__OPMODES[self.mode]
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._status, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
return 'HeatCirculationPumpMode: {0} ({1})'.format(self.mode, self.modeAsString)
def parseToJSON(self):
from json import dumps
jsonObj = {};
jsonObj['mode'] = {
'status': self._status,
'timestampMicros': self._timestampMicros,
'timePrecision': self._timePrecision,
'quality': self._quality,
'validity': self._validity,
'source': self._source,
}
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None ## Appears unused
try:
jsonObj = loads(jsonString).get('mode')
return HeatCirculationPumpMode(
jsonObj.get('status'),
jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'),
jsonObj.get('validity'),
jsonObj.get('source'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,83 @@
class HeatCirculationPumpState:
__OPSTATES = {
-1: 'PUMP_STATE_UNKNOWN',
0: 'PUMP_STATE_STOPPED',
1: 'PUMP_STATE_RUNNING',
2: 'PUMP_STATE_ERROR',}
__OPSTATES_R = {v: k for k, v in __OPSTATES.items()}
def __init__(self, status, timestampMicros, timePrecision=0, quality=0, validity=0, source=0):
"""
@Input:
status (int or str): Pump operating mode, can be both int and string
To get list of available modes: HeatCirculationPumpState.opstates()
"""
if status in HeatCirculationPumpState.__OPSTATES:
self._status = status
elif state in HeatCirculationPumpState.__OPSTATES_R:
self._status = HeatCirculationPumpState.__OPSTATES_R[status]
else:
raise AssertionError('HeatCirculationPumpState not recognized: \'{0}\''.format(mode))
self._timestampMicros = timestampMicros
self._timePrecision = timePrecision
self._quality = quality
self._validity = validity
self._source = source
@staticmethod
def opstates():
return HeatCirculationPumpState.__OPSTATES.copy()
@property
def state(self):
return self._status
@property
def stateAsString(self):
return HeatCirculationPumpState.__OPSTATES[self._status]
def __repr__(self):
return "{0}(".format(self.__class__.__name__) + \
"{0!r}, {1!r}, ".format(self._status, self._timestampMicros) + \
"{0!r}, {1!r}, ".format(self._timePrecision, self._quality) + \
"{0!r}, {1!r})".format(self._validity, self._source)
def __str__(self):
return 'HeatCirculationPumpState: {0} ({1})'.format(self.state, self.stateAsString)
def parseToJSON(self):
from json import dumps
jsonObj = {}
jsonObj['state'] = {
'status': self._status,
'timestampMicros': self._timestampMicros,
'timePrecision': self._timePrecision,
'quality': self._quality,
'validity': self._validity,
'source': self._source,
}
return dumps(jsonObj)
@staticmethod
def parseFromJSON(jsonString):
from json import JSONDecodeError, loads
if not type(jsonString) is str: raise TypeError('jsonString should be a string, found {0}'.format(type(jsonString)))
# result = None ## Appears unused
try:
jsonObj = loads(jsonString).get('state')
return HeatCirculationPumpState(
jsonObj.get('status'),
jsonObj.get('timestampMicros'),
jsonObj.get('timePrecision'),
jsonObj.get('quality'),
jsonObj.get('validity'),
jsonObj.get('source'))
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(jsonString))
raise e

View File

@ -0,0 +1,73 @@
from dataclasses import dataclass
from . import CompositeMeasurement
from typing import Optional
# TODO: Add processing to these.
@dataclass
class DELV:
phaseAB: CompositeMeasurement
phaseBC: CompositeMeasurement
phaseCA: CompositeMeasurement
phaseAverage: CompositeMeasurement
@staticmethod
def parseFromJSON(json_string):
from json import JSONDecodeError, loads
if not type(json_string) is str:
raise TypeError('jsonString should be a string, found {0}'.format(type(json_string)))
try:
jsonObj = loads(json_string)
phaseABcm = CompositeMeasurement(**jsonObj.get('phaseAB'))
phaseBCcm = CompositeMeasurement(**jsonObj.get('phaseBC'))
phaseCAcm = CompositeMeasurement(**jsonObj.get('phaseCA'))
phaseAVGcm = CompositeMeasurement(**jsonObj.get('phaseAverage'))
return DELV(phaseABcm, phaseBCcm, phaseCAcm, phaseAVGcm)
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(json_string))
raise e
class GPSL:
pass
class HLTH:
pass
class LNPL:
pass
class PNPL:
pass
@dataclass()
class WYEA:
phaseAB: CompositeMeasurement
phaseBC: CompositeMeasurement
phaseCA: CompositeMeasurement
neutral: Optional[CompositeMeasurement] = None
@staticmethod
def parseFromJSON(json_string):
from json import JSONDecodeError, loads
if not type(json_string) is str:
raise TypeError('jsonString should be a string, found {0}'.format(type(json_string)))
try:
jsonObj = loads(json_string)
phaseAcm = CompositeMeasurement(**jsonObj.get('phaseA'))
phaseBcm = CompositeMeasurement(**jsonObj.get('phaseB'))
phaseCcm = CompositeMeasurement(**jsonObj.get('phaseC'))
neutral = jsonObj.get('neutral')
if neutral is not None:
neutral = CompositeMeasurement(**neutral)
return WYEA(phaseAcm, phaseBcm, phaseCcm, neutral)
except JSONDecodeError as e:
print('Not a valid JSON string: {0}.'.format(json_string))
raise e

View File

@ -0,0 +1,16 @@
__all__ = []
# Composite Data Types
from .CompositeBoolean import CompositeBoolean
from .CompositeMeasurement import CompositeMeasurement
from .CompositeStatus import CompositeStatus
# SYSLAB-specific data types
from .Identifiers import DELV, GPSL, HLTH, LNPL, PNPL, WYEA
# Unit-specific data types
from .BattOpMode import BattOpMode
from .FlowBatteryState import FlowBatteryState
from .HeatCirculationPumpMode import HeatCirculationPumpMode
from .HeatCirculationPumpState import HeatCirculationPumpState
from .ConverterTypes import ConvState, ConvOpMode

View File

@ -0,0 +1,27 @@
def get_flexhouse(simulated=False, physical=False, simulate_on_dumpload_ID="", simulate_on_battery=False):
"""
Return an instantiated object which operates as a flexhouse.
input:
simulated (bool): Whether the flexhouse should be simulated
physical (bool): Whether the flexhouse should be the real flexhouse
simulate_on_dumpload_ID (string): The ID of the dumpload on which to simulate the flexhouse
simulate_on_battery (bool): Whether to simulate on battery
return:
flexhouse: A flexhouse object
"""
if simulated:
if simulate_on_battery:
from .virtual.FlexHouse_sim_batt import FlexHouse_sim_batt
return FlexHouse_sim_batt('batt1')
else:
from .virtual.FlexHouse_sim import FlexHouse_sim
assert simulate_on_dumpload_ID != "", "Must supply an ID string for the dumpload used in Flexhouse simulation if not simulating on battery"
return FlexHouse_sim(simulate_on_dumpload_ID)
elif physical:
from .physical.FlexHouse_real import FlexHouse_real
return FlexHouse_real()
else:
raise Exception('Must define if FlexHouse instance is real or simulated')

View File

@ -0,0 +1,258 @@
from ..core.SyslabUnit import SyslabUnit
from ..core.datatypes import CompositeMeasurement, CompositeBoolean, CompositeStatus
from ..core.datatypes import CommonDeviceConfig
from ..core.datatypes import DELV, WYEA, GPSL, PNPL, LNPL, HLTH, ConvOpMode, ConvState
from typing import List, Union
from time import time
def cast_to_cm(m: Union[CompositeMeasurement, float]):
if type(m) == float:
# TODO: Is there a better way to estimate precision of time.time?
request = CompositeMeasurement(m, timestampMicros=time()*1e6, timePrecision=1000)
elif type(m) == CompositeMeasurement:
request = m
else:
raise TypeError(f"Unknown request type: {type(m)}")
return request
class B2BConverter(SyslabUnit):
"""
Class covering back-to-back converters in SYSLAB.
A full list of available switchboards can be found by calling 'SwitchBoard.getAvailableSwitchBoards()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
_CONVERTERS = {
'ABBB2B': ('syslab-04.syslab.dk', '8080', 'B2BConverter'),
}
MAXP = 60
MAXQ = 60
@staticmethod
def getAvailableUnits():
return list(B2BConverter.__CONVERTERS.keys())
def __init__(self, which=None, host=None, port=None, unit_name=None):
baseurl = 'http://{host}:{port}/typebased_WebService_ABBConverter/ABBConverterWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self._CONVERTERS,
host=host,
port=port,
unit_name=unit_name,
unit_type="B2BConverter")
# Inventory functions
# Configuration class used for HMI layout
def getNodeConfiguration(self, ) -> CommonDeviceConfig:
return self._request_resource('getNodeConfiguration')
# Component description
def getConverterName(self, ) -> str:
return self._request_resource('getConverterName')
def getConverterLogicalNameplate(self, ) -> LNPL:
raise NotImplementedError
def getConverterPhysicalNameplate(self, ) -> PNPL:
raise NotImplementedError
def getConverterHealth(self, ) -> HLTH:
raise NotImplementedError
def getGPSLocation(self, ) -> GPSL:
raise NotImplementedError
# Operating characteristics (corresponding to COPR/DRAT node)
def getRatedP(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedP')
def getRatedS(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedS')
def getRatedQ(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedQ')
def getRatedU(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedU')
def getRatedI(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedI')
def getRatedf(self, ) -> float:
raise NotImplementedError
# return self._request_resource('getRatedf')
# Operating mode settings (corresponding to DOPM node)
def getAvailableOperatingModes(self, ) -> List[ConvOpMode]:
raise NotImplementedError
def getCurrentOperatingMode(self, ) -> ConvOpMode:
return self._request_resource('getCurrentOperatingMode')
def setOperatingMode(self, mode: ConvOpMode) -> None:
raise NotImplementedError
def isPSetpointEnabled(self, ) -> CompositeBoolean:
return self._request_resource('isPSetpointEnabled')
def isQSetpointEnabled(self, ) -> CompositeBoolean:
return self._request_resource('isQSetpointEnabled')
def isUSetpointEnabled(self, ) -> CompositeBoolean:
return self._request_resource('isUSetpointEnabled')
def isfSetpointEnabled(self, ) -> CompositeBoolean:
return self._request_resource('isfSetpointEnabled')
# Status information (corresponding to DPST node)
def getConverterStatus(self, ) -> ConvState:
return self._request_resource('getConverterStatus')
# Alarms information
def hasActiveFault(self, ) -> CompositeBoolean:
raise NotImplementedError
def hasActiveWarning(self, ) -> CompositeBoolean:
raise NotImplementedError
def resetAlarms(self, ) -> None:
raise NotImplementedError
def getActiveEventCode(self, ) -> CompositeStatus:
raise NotImplementedError
# DER controller characteristics (corresponding to DRCT node)
def setActivePower(self, m: Union[CompositeMeasurement, float]) -> None:
"""
Send a request for the converter's output active power. Requires the converter to be in PQ mode.
:param m: [kW] Requested active power. If a float, will be converted to a CompositeMeasurement with the current system time as timestamp.)
:return: None
"""
P_UpperLimit = 15.0
P_LowerLimit = -15.0
return self._request_resource('setActivePower', (), 'put', cast_to_cm(max(P_LowerLimit, min(m, P_UpperLimit))).parseToJSON())
def setFrequency(self, m: Union[CompositeMeasurement, float]) -> None:
"""
Send a request for the converter's output power. Requires the converter to be in UF mode.
:param m: [Hz] Requested active power. If a float, will be converted to a CompositeMeasurement with the current system time as timestamp.)
:return: None
"""
return self._request_resource('seFrequency', (), 'put', cast_to_cm(m).parseToJSON())
def setReactivePower(self, m: Union[CompositeMeasurement, float]) -> None:
"""
Send a request for the converter's output reactive power. Requires the converter to be in PQ mode.
:param m: [kVA] Requested active power. If a float, will be converted to a CompositeMeasurement with the current system time as timestamp.)
:return: None
"""
Q_UpperLimit = 15.0
Q_LowerLimit = -15.0
return self._request_resource('setReactivePower', (), 'put', cast_to_cm(max(Q_LowerLimit, min(m, Q_UpperLimit))).parseToJSON())
def setVoltage(self, m: Union[CompositeMeasurement, float]) -> None:
"""
Send a request for the converter's output voltage. Requires the converter to be in UF mode.
:param m: [V] Requested active power. If a float, will be converted to a CompositeMeasurement with the current system time as timestamp.)
:return: None
"""
return self._request_resource('setVoltage', (), 'put', cast_to_cm(m).parseToJSON())
def getActivePowerSetpoint(self, ) -> CompositeMeasurement:
raise NotImplementedError
def getFrequencySetpoint(self, ) -> CompositeMeasurement:
raise NotImplementedError
def getReactivePowerSetpoint(self, ) -> CompositeMeasurement:
raise NotImplementedError
def getVoltageSetpoint(self, ) -> CompositeMeasurement:
raise NotImplementedError
# Synchronisation (corresponding to RSYN node)
def synchronize(self, ) -> None:
raise NotImplementedError
def unsynchronize(self, ) -> None:
raise NotImplementedError
def setDroopEnable(self, b: CompositeBoolean) -> None:
raise NotImplementedError
def setLoadEnable(self, b: CompositeBoolean) -> None:
raise NotImplementedError
def getVoltageDroopPct(self, ) -> CompositeMeasurement:
raise NotImplementedError
def getFrequencyDroopPct(self, ) -> CompositeMeasurement:
raise NotImplementedError
def setVoltageDroopPct(self, pct: CompositeMeasurement) -> None:
raise NotImplementedError
def setFrequencyDroopPct(self, pct: CompositeMeasurement) -> None:
raise NotImplementedError
# Reciprocating Engine (corresponds to DCIP node)
def startConverter(self, ) -> None:
return self._request_resource('startConverter', (), 'put')
def softStopConverter(self, ) -> None:
return self._request_resource('softStopConverter', (), 'put')
def stopConverter(self, ) -> None:
return self._request_resource('stopConverter', (), 'put')
# AC quantities (corresponds to MMXU nodes)
def getActivePowerOutput(self, ) -> CompositeMeasurement:
return self._request_resource('getActivePowerOutput')
def getReactivePowerOutput(self, ) -> CompositeMeasurement:
return self._request_resource('getReactivePowerOutput')
def getOutputFrequency(self, ) -> CompositeMeasurement:
return self._request_resource('getOutputFrequency')
def getOutputInterphaseVoltages(self, ) -> DELV:
return self._request_resource('getOutputInterphaseVoltages')
def getOutputPhaseCurrents(self, ) -> WYEA:
raise NotImplementedError
def getRectifierInterphaseVoltages(self, ) -> DELV:
return self._request_resource('getRectifierInterphaseVoltages')
def getRectifierPhaseCurrents(self, ) -> WYEA:
return self._request_resource('getRectifierPhaseCurrents')
def getSyncBusInterphaseVoltages(self, ) -> DELV:
raise NotImplementedError
class ABBB2BConverter(B2BConverter):
def __init__(self):
super(ABBB2BConverter, self).__init__(which='ABBB2B')

View File

@ -0,0 +1,85 @@
from ..core.SyslabUnit import SyslabUnit
from ..core.datatypes.FlowBatteryState import FlowBatteryState
from ..core.datatypes.BattOpMode import BattOpMode
class Battery(SyslabUnit):
"""The Battery class represents a battery in SYSLAB.
The Battery class is instantiated using a string with the unique name of the battery, ie. 'which'
A full list of available batteries can be found by calling 'Battery.getAvailableBatteries()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__BATTERIES = {
'batt1': ('syslab-12.syslab.dk', '8080', 'batt1'),
'battemu': ('syslab-31.syslab.dk', '8080', 'battemu'),
'battfh1': ('syslab-s01.syslab.dk', '8080', 'battfh1'),
'simlab-15': ('192.168.0.115', '8080', 'batt1'),
'vbatt1': ('simlab-12', '8080', 'batt1'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_Battery/VRBBatteryWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__BATTERIES,
host=host,
port=port,
unit_name=unitname,
unit_type="Battery")
def startBattery(self):
return self._request_resource('startBattery', (), 'put')
def stopBattery(self):
return self._request_resource('stopBattery', (), 'put')
def getActivePower(self):
return self._request_resource('getACActivePower')
def setActivePower(self, setPoint):
P_UpperLimit = 15.0
P_LowerLimit = -15.0
return self._request_resource('setP', (max(P_LowerLimit, min(setPoint, P_UpperLimit))), 'put')
def getReactivePower(self):
return self._request_resource('getACReactivePower')
def setReactivePower(self, setPoint):
return self._request_resource('setQ', (setPoint), 'put')
def getFrequency(self):
return self._request_resource('getACFrequency')
def getRemainingFloodTime(self):
return min(self._request_resource('getRemainingFloodTime'), self._request_resource('getRemainingDrainTime'))
def getCurrentOperatingMode(self):
return self._request_resource('getCurrentOperatingMode')
def getCurrentOperatingState(self):
return self._request_resource('getCurrentOperatingState')
def setOperatingMode(self, mode):
return self._request_resource('setOperatingMode', (), 'put', BattOpMode(mode).parseToJSON())
def getSOC(self):
return self._request_resource('getSOC')
def getRatedActivePower(self):
return self._request_resource('getRatedP')
def getRatedReactivePower(self):
return self._request_resource('getRatedQ')
def getName(self):
return self._request_resource('getBatteryName')
@staticmethod
def getAvailableBatteries():
return list(Battery.__BATTERIES.keys())

View File

@ -0,0 +1,100 @@
from ..core.SyslabUnit import SyslabUnit
class DieselGenerator(SyslabUnit):
"""The DieselGenerator class represents a photovoltaic panel array in SYSLAB.
The DieselGenerator class is instantiated using a string with the unique name of the dumpload, ie. 'which'
A full list of available panel arrays can be found by calling 'DieselGenerator.getAvailableDieselGenerator()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__UNITS = {
'diesel319': ('syslab-02.syslab.dk', '8080', 'genset1'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_Diesel/DEIFDieselGensetWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__UNITS,
host=host,
port=port,
unit_name=unitname,
unit_type="DieselGenerator")
def getRatedP(self):
return self._request_resource('getRatedP')
def getRatedQ(self):
return self._request_resource('getRatedQ')
def getRatedS(self):
return self._request_resource('getRatedS')
def getRatedU(self):
return self._request_resource('getRatedU')
def getRatedf(self):
return self._request_resource('getRatedf')
def isGeneratorBreakerClosed(self):
return self._request_resource('isGeneratorBreakerClosed')
def isGensetRunning(self):
return self._request_resource('isGensetRunning')
def isGensetSynchronized(self):
return self._request_resource('isGensetSynchronized')
def isSynchronising(self):
return self._request_resource('isSynchronising')
def closeGB(self):
return self._request_resource('closeGB', method='put')
def openGB(self):
return self._request_resource('openGB', method='put')
def startGenset(self):
return self._request_resource('startGenset', method='put')
def stopGenset(self):
return self._request_resource('stopGenset', method='put')
def setTargetActivePower(self, setpoint):
return self._request_resource('setTargetActivePower', setpoint, 'put')
def setTargetReactivePower(self, setpoint):
return self._request_resource('setTargetReactivePower', setpoint, 'put')
def getActivePower(self):
return self._request_resource('getActivePower')
def getReactivePower(self):
return self._request_resource('getReactivePower')
#TODO: Implement
def getCurrentGensetMode(self):
pass
#TODO: Implement
def getCurrentRunningMode(self):
pass
#TODO: Implement
def setGensetMode(self, mode):
pass
#TODO: Implement
def setRunningMode(self, mode):
pass
@staticmethod
def getAvailableDieselGenerators():
return list(DieselGenerator.__UNITS.keys())

View File

@ -0,0 +1,112 @@
from ..core.SyslabUnit import SyslabUnit
import warnings
class Dumpload(SyslabUnit):
"""The Dumpload class represents a dumpload in SYSLAB.
The Dumpload class is instantiated using a string with the unique name of the dumpload, ie. 'which'
A full list of available dumploads can be found by calling 'Dumpload.getAvailableDumpLoads()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__DUMPLOADS = {
'mobload1': ('syslab-16.syslab.dk', '8080', 'mobload1'),
'mobload2': ('syslab-17.syslab.dk', '8080', 'mobload2'),
'mobload3': ('syslab-18.syslab.dk', '8080', 'mobload3'),
'load1': ('syslab-05.syslab.dk', '8080', 'load1'),
'vmobload1': ('simlab-16', '8080', 'mobload1'),
'vmobload2': ('simlab-17', '8080', 'mobload2'),
'vmobload3': ('simlab-18', '8080', 'mobload3'),
'vload1': ('simlab-05', '8080', 'load1'),
'simlab-05': ('192.168.0.105', '8080', 'mobload2'),
'simlab-11': ('192.168.0.111', '8080', 'mobload1'),
'simlab-12': ('192.168.0.112', '8080', 'load1'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_Load/GenericLoadWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__DUMPLOADS,
host=host,
port=port,
unit_name=unitname,
unit_type="Battery")
def startLoad(self):
return self._request_resource('startLoad', (), 'put')
def stopLoad(self):
return self._request_resource('stopLoad', (), 'put')
def getPowerSetPoint(self):
return self._request_resource('getConstantP')
def setPowerSetPoint(self, setPoint):
"""
Set the active power setpoint for the load.
Inputs:
setPoint (float): Requested power setpoint
Outputs:
Ack (bool): Acknowledgement of receiver
"""
P_UpperLimit = 15.0
P_LowerLimit = 0
return self._request_resource('setConstantP', max(P_LowerLimit, min(setPoint, P_UpperLimit)), 'put')
def getActivePower(self):
return self._request_resource('getActivePower')
def getReactivePower(self):
"""
Get the reactive power draw from the load.
Outputs:
Q (CompositeMeasurement): Current reactive power draw, calculated from active power draw (see note)
NOTE: This is a theoretical value calculated from the relation
Q = Q_r * sin (pi * P /P_r)
where P_r and Q_r are the rated active and reactive power draw.
For control purposes, use the measured value from the switchboard
instead to get an actual measurement.
"""
warnings.warn("The output of getReactivePower from the Dumpload class is calculated from the active power draw rather than a measured value. For control purposes, use the measured power draw on the switchboard.")
return self._request_resource('getReactivePower')
def getRatedPower(self):
return self._request_resource('getRatedP')
def getRatedReactivePower(self):
return self._request_resource('getRatedQ')
def getName(self):
return self._request_resource('getLoadName')
def isLoadOn(self):
return self._request_resource('isLoadOn')
@staticmethod
def getAvailableDumploads():
return list(Dumpload.__DUMPLOADS.keys())
class MobileLoad1(Dumpload):
def __init__(self):
super(MobileLoad1, self).__init__("mobload1")
class MobileLoad2(Dumpload):
def __init__(self):
super(MobileLoad2, self).__init__("mobload2")
class MobileLoad3(Dumpload):
def __init__(self):
super(MobileLoad3, self).__init__("mobload3")

View File

@ -0,0 +1,213 @@
from ..core.SyslabUnit import SyslabUnit
from ..core.datatypes import CompositeMeasurement
from typing import Union
from time import time
def cast_to_cm(m: Union[CompositeMeasurement, float]):
if type(m) == float:
# TODO: Is there a better way to estimate precision of time.time?
request = CompositeMeasurement(m, timestampMicros=time()*1e6, timePrecision=1000)
elif type(m) == CompositeMeasurement:
request = m
else:
raise TypeError(f"Unknown request type: {type(m)}")
return request
class EVSE(SyslabUnit):
"""The EVSE class represents a charging post in SYSLAB.
The EVSE class is instantiated using a string with the unique name of the charging post, ie. 'which'
A full list of available charging posts can be found by calling 'EVSE.getAvailableChargingPosts()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__CHARGINGPOSTS = {
'V2G-319': ('syslab-35.syslab.dk', '8080', 'Endesa_V2G'),
'EVSE-NEVIC-6': ('10.42.245.96', '8080', 'EVSE_NEVIC_6'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_EVSE/EVSEWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__CHARGINGPOSTS,
host=host,
port=port,
unit_name=unitname,
unit_type="EVSE")
def isEVPresent(self):
return self._request_resource('isEVPresent')
def canRemoteStartStop(self):
return self._request_resource('canRemoteStartStop')
def canLimitChargePower(self):
return self._request_resource('canLimitChargePower')
def canDischarge(self):
return self._request_resource('canDischarge')
def hasSOC(self):
return self._request_resource('hasSOC')
def hasACMeasurements(self):
return self._request_resource('hasACMeasurements')
def hasDCMeasurements(self):
return self._request_resource('hasDCMeasurements')
def getAvailableOperatingStates(self):
return self._request_resource('getAvailableOperatingStates')
def getCurrentOperatingState(self):
return self._request_resource('getCurrentOperatingState')
def getACActivePower(self):
return self._request_resource('getACActivePower')
def getACReactivePower(self):
return self._request_resource('getACReactivePower')
def getACFrequency(self):
return self._request_resource('getACFrequency')
def getACVoltages(self):
return self._request_resource('getACVoltages')
def getACCurrents(self):
return self._request_resource('getACCurrents')
def getSOC(self):
return self._request_resource('getSOC')
def getMinSOC(self):
return self._request_resource('getMinSOC')
def getMaxSOC(self):
return self._request_resource('getMaxSOC')
def startCharge(self):
return self._request_resource('startCharge', (), 'put')
def stopCharge(self):
return self._request_resource('stopCharge', (), 'put')
def startDischarge(self):
return self._request_resource('startDischarge', (), 'put')
def stopDischarge(self):
return self._request_resource('stopDischarge', (), 'put')
def getMinimumChargePower(self):
return self._request_resource('getMinimumChargePower')
def getMaximumChargePower(self):
return self._request_resource('getMaximumChargePower')
def getMinimumDischargePower(self):
return self._request_resource('getMinimumDischargePower')
def getMaximumDischargePower(self):
return self._request_resource('getMaximumDischargePower')
def setPowerSetpoint(self, setPoint):
P_UpperLimit = 10.0
P_LowerLimit = -10.0
return self._request_resource('setP', (), 'put', cast_to_cm(max(P_LowerLimit, min(setPoint, P_UpperLimit))).parseToJSON())
def isPSetpointEnabled(self):
return self._request_resource('isPSetpointEnabled')
def getActiveEnergyImport(self): # TODO not working/implemented
return self._request_resource('getActiveEnergyImport')
def getActiveEnergyExport(self): # TODO not working/implemented
return self._request_resource('getActiveEnergyExport')
def getReactiveEnergyImport(self): # TODO not working/implemented
return self._request_resource('getReactiveEnergyImport')
def getReactiveEnergyExport(self): # TODO not working/implemented
return self._request_resource('getReactiveEnergyExport')
def getNodeConfiguration(self): # TODO not working/implemented
return self._request_resource('getNodeConfiguration')
def getEVSEName(self):
return self._request_resource('getEVSEName')
def getEVSELogicalNameplate(self): # TODO not working/implemented
return self._request_resource('getEVSELogicalNameplate')
def getEVSEPhysicalNameplate(self): # TODO not working/implemented
return self._request_resource('getEVSEPhysicalNameplate')
def getEVSEHealth(self): # TODO not working/implemented
return self._request_resource('getEVSEHealth')
def getGPSLocation(self): # TODO not working/implemented
return self._request_resource('getGPSLocation')
def getNumberOfEvents(self): # TODO not working/implemented
return self._request_resource('getNumberOfEvents')
def getEvents(self): # TODO not working/implemented
return self._request_resource('getEvents')
def getNumberOfAlarms(self): # TODO not working/implemented
return self._request_resource('getNumberOfAlarms')
def getAlarms(self): # TODO not working/implemented
return self._request_resource('getAlarms')
def getNumberOfUnacknowledgedAlarms(self): # TODO not working/implemented
return self._request_resource('getNumberOfUnacknowledgedAlarms')
def getUnacknowledgedAlarms(self): # TODO not working/implemented
return self._request_resource('getUnacknowledgedAlarms')
def acknowledgeAlarms(self): # TODO not working/implemented
return self._request_resource('acknowledgeAlarms')
def getRatedP(self):
return self._request_resource('getRatedP')
def getRatedQ(self):
return self._request_resource('getRatedQ')
def getRatedS(self):
return self._request_resource('getRatedS')
def getRatedU(self):
return self._request_resource('getRatedU')
def getRatedf(self):
return self._request_resource('getRatedf')
def getInverterName(self): # TODO not working/implemented
return self._request_resource('getInverterName')
def getInverterLogicalNameplate(self): # TODO not working/implemented
return self._request_resource('getInverterLogicalNameplate')
def getInverterHealth(self): # TODO not working/implemented
return self._request_resource('getInverterHealth')
def getDCVoltage(self): # TODO not working/implemented
return self._request_resource('getDCVoltage')
def getDCPower(self): # TODO not working/implemented
return self._request_resource('getDCPower')
def isDCContactorClosed(self): # TODO not working/implemented
return self._request_resource('isDCContactorClosed')
@staticmethod
def getAvailableEVSE():
return list(EVSE.__CHARGINGPOSTS.keys())

View File

@ -0,0 +1,164 @@
from ..core.SyslabUnit import SyslabUnit
from ..core.datatypes import CompositeMeasurement, HeatCirculationPumpMode, HeatCirculationPumpState
class Valve:
"""
Convenience class which calls methods on its associated heatswitchboard
"""
def __init__(self, label, heatswitchboard):
self.label = label
self.heatswitchboard = heatswitchboard
#TODO: Add methods
class Pump:
"""
Convenience class which calls methods on its associated heatswitchboard
"""
def __init__(self, label, heatswitchboard):
self.label = label
self.heatswitchboard = heatswitchboard
#TODO: Add methods
class Meter:
"""
Convenience class which calls methods on its associated heatswitchboard
"""
def __init__(self, label, heatswitchboard):
self.label = label
self.heatswitchboard = heatswitchboard
#TODO: Add methods
class HeatSwitchBoard(SyslabUnit):
"""
The HeatSwitchBoard class represents a HeatSwitchBoard in SYSLAB.
The HeatSwitchBoard class is instantiated using a string with the unique name of the switchboard, ie. 'which'
A full list of available switchboards can be found by calling 'SwitchBoard.getAvailableSwitchBoards()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__HEAT_SWITCH_BOARDS = {
'716-h1': ('syslab-33.syslab.dk', '8080', '716-h1'),
}
@staticmethod
def getAvailableHeatSwitchBoards():
return list(HeatSwitchBoard.__HEAT_SWITCH_BOARDS.keys())
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = f'http://{host}:{port}/typebased_WebService_HeatSubstation/HeatSwitchboardWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__HEAT_SWITCH_BOARDS,
host=host,
port=port,
unit_name=unitname,
unit_type="HeatSwitchBoard")
# Inventory functions
#TODO Handle these in core/SYSLAB_unit.py by properly handling arrays. This is a bodge for now.
def getHeatMeters(self):
result = self._request_resource('getHeatMeters', check_types=False)
return result
#TODO, P1: Awaiting Valve[]
def getValves(self):
raise NotImplementedError
# Valve functions
def getValvePosition(self, valve) -> CompositeMeasurement:
return self._request_resource('getValvePosition', valve)
#TODO: Implement
#def setValvePosition(self, valve: str, position: float, timestamp=0):
def setValvePosition(self, valve: str, position: CompositeMeasurement):
"""
Sets *valve* to *position*. Position must be float in the
interval from 0.0 to 1.0
Position = 0.0 => *valve* fully closed
Position = 1.0 => *valve* fully open
"""
assert 0.0 <= position.value and position.value <= 1.0
return self._request_resource('setValvePosition', (valve), 'put',
position.parseToJSON())
#CompositeMeasurement(value=position, timestampMicros=timestamp).parseToJSON())
# Meter functions
def getBackTemperature(self, meter) -> CompositeMeasurement:
return self._request_resource('getBackTemperature', meter)
def getFlow(self, meter) -> CompositeMeasurement:
return self._request_resource('getFlow', meter)
def getThermalPower(self, meter) -> CompositeMeasurement:
return self._request_resource('getThermalPower', meter)
def getFwdTemperature(self, meter) -> CompositeMeasurement:
return self._request_resource('getFwdTemperature', meter)
def getPressure(self, meter) -> CompositeMeasurement:
return self._request_resource('getPressure', meter)
def getVolume(self, meter) -> CompositeMeasurement:
return self._request_resource('getVolume', meter)
def getMass(self, meter) -> CompositeMeasurement:
return self._request_resource('getMass', meter)
def getHeatEnergy(self, meter) -> CompositeMeasurement:
return self._request_resource('getHeatEnergy', meter)
def getCoolingEnergy(self, meter) -> CompositeMeasurement:
return self._request_resource('getCoolingEnergy', meter)
# Pump functions
def getPumpState(self, pump) -> HeatCirculationPumpState:
return self._request_resource('getPumpState', pump)
def getPumpControlMode(self, pump) -> HeatCirculationPumpMode:
return self._request_resource('getPumpControlMode', pump)
def setPumpControlMode(self, pump, mode: HeatCirculationPumpMode):
return self._request_resource('setPumpControlMode', (pump), 'put', mode.parseToJSON())
def setPumpMaxFlow(self, pump, limit: CompositeMeasurement):
return self._request_resource('setPumpMaxFlow', (pump), 'put', limit.parseToJSON())
def startPump(self, pump):
return self._request_resource('startPump', pump)
def stopPump(self, pump):
return self._request_resource('stopPump', pump)
#TODO: Split into three functions that check against current pump mode.
def setPumpSetpoint(self, pump, setpoint: CompositeMeasurement):
"""
Sets the target for the pump.
NOTE: How to interpret the setpoint depends on the mode which the pump is in.
Here be dragons.
"""
return self._request_resource('setPumpSetpoint', (pump), 'put', setpoint.parseToJSON())
def getPumpHead(self, pump) -> CompositeMeasurement:
return self._request_resource('getPumpHead', pump)
def getPumpFlow(self, pump) -> CompositeMeasurement:
return self._request_resource('getPumpFlow', pump)
def getPumpRPM(self, pump) -> CompositeMeasurement:
return self._request_resource('getPumpRPM', pump)
def getPumpRelPerformance(self, pump) -> CompositeMeasurement:
return self._request_resource('getPumpPerformance', pump)

View File

@ -0,0 +1,72 @@
from ..core.SyslabUnit import SyslabUnit
class MeteoMast(SyslabUnit):
__METMASTS = {
'metmast1': ('syslab-13.syslab.dk', '8080', 'meteo1'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_Meteo/MeteoStationWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__METMASTS,
host=host,
port=port,
unit_name=unitname,
unit_type="MeteoMast")
def getAirPressure(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getAirPressure2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getAirPressure1', instrumentIndex)
def getAirTemperature(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getAirTemperature2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getAirTemperature1', instrumentIndex)
def getHeightAboveGround(self):
return self._request_resource('getHeightAboveGround')
def getInsolation(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getInsolation2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getInsolation1', instrumentIndex)
def getMeteoGPSLocation(self):
return self._request_resource('getMeteoGPSLocation')
def getRelativeHumidity(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getRelativeHumidity2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getRelativeHumidity1', instrumentIndex)
def getWindDirection(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getWindDirection2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getWindDirection1', instrumentIndex)
def getWindSpeed(self, instrumentIndex: int or str):
if type(instrumentIndex) is str:
return self._request_resource('getWindSpeed2', instrumentIndex)
if type(instrumentIndex) is int:
return self._request_resource('getWindSpeed1', instrumentIndex)
def getInstrumentNames(self):
return self._request_resource('getInstrumentNames')
def getSupportedInstrumentTypes(self):
return self._request_resource('getSupportedInstrumentTypes')
def getInstrumentNamesForType(self, instrumentType: str):
return self._request_resource('getInstrumentNamesForType', instrumentType)

View File

@ -0,0 +1,61 @@
from ..core.SyslabUnit import SyslabUnit
class Photovoltaics(SyslabUnit):
"""The Photovoltaics class represents a photovoltaic panel array in SYSLAB.
The Photovoltaics class is instantiated using a string with the unique name of the dumpload, ie. 'which'
A full list of available panel arrays can be found by calling 'Photovoltaics.getAvailablePhotovoltaics()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__PHOTOVOLTAICS = {
'pv319': ('syslab-24.syslab.dk', '8080', 'pv319'),
'pv715': ('syslab-10.syslab.dk', '8080', 'pv715'),
'pv117': ('syslab-07.syslab.dk', '8080', 'pv117'),
'simlab-03': ('192.168.0.103', '8080', 'pv715'),
'simlab-13': ('192.168.0.113', '8080', 'pv319'),
'vpv319': ('simlab-24', '8080', 'pv319'),
'vpv715': ('simlab-10', '8080', 'pv715'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_PV/PVSystemWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__PHOTOVOLTAICS,
host=host,
port=port,
unit_name=unitname,
unit_type="Photovoltaics")
def getACActivePower(self):
return self._request_resource('getACActivePower')
def getACReactivePower(self):
return self._request_resource('getACReactivePower')
def getPacLimit(self):
return self._request_resource('getPacLimit')
def getQSetpoint(self):
return self._request_resource('getQSetpoint')
def getRatedPower(self):
return self._request_resource('getRatedP')
def getName(self):
return self._request_resource('getSystemName')
def setPacLimit(self, setPoint):
return self._request_resource('setPacLimit', setPoint, 'put')
def setQSetpoint(self, Q):
return self._request_resource('setQ', Q, 'put')
@staticmethod
def getAvailablePhotovoltaics():
return list(Photovoltaics.__PHOTOVOLTAICS.keys())

View File

@ -0,0 +1,144 @@
from ..core.SyslabUnit import SyslabUnit
class SwitchBoard(SyslabUnit):
"""
The SwitchBoard class represents a SwitchBoard in SYSLAB.
The SwitchBoard class is instantiated using a string with the unique name of the switchboard, ie. 'which'
A full list of available switchboards can be found by calling 'SwitchBoard.getAvailableSwitchBoards()'
Alternatively, the user may specify a host and port to connect to via the *host* and *port* arguments.
"""
__SWITCH_BOARDS = {
'319-2':('syslab-01.syslab.dk', '8080', '319-2'),
'319-3':('syslab-52.syslab.dk', '8080', '319-3'),
'117-2':('syslab-11.syslab.dk', '8080', '117-2'),
'117-4':('syslab-11.syslab.dk', '8080', '117-4'),
'117-5':('syslab-11.syslab.dk', '8080', '117-5'),
'117-6':('syslab-26.syslab.dk', '8080', '117-6'),
'715-2':('syslab-09.syslab.dk', '8080', '715-2'),
'716-2':('syslab-29.syslab.dk', '8080', '716-2'),
'simlab-00': ('192.168.0.1', '8080', '319-2'),
'simlab-10': ('192.168.0.2', '8080', '319-2'),
'simlab-20': ('192.168.0.3', '8080', '319-2'),
'vswitchboard': ('simlab-01', '8080', '319-2')
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_Substation/StandardSubstationWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__SWITCH_BOARDS,
host=host,
port=port,
unit_name=unitname,
unit_type="SwitchBoard")
def getName(self):
return self._request_resource('getSwitchboardName')
def getActivePower(self, instrName):
return self._request_resource('getActivePower', instrName)
def getPhaseActivePower(self, instrName):
return self._request_resource('getPhaseActivePower', instrName)
def getReactivePower(self, instrName):
return self._request_resource('getReactivePower', instrName)
def getPhaseReactivePower(self, instrName):
return self._request_resource('getPhaseReactivePower', instrName)
def getApparentPower(self, instrName):
return self._request_resource('getApparentPower', instrName)
def getPhaseApparentPower(self, instrName):
return self._request_resource('getPhaseApparentPower', instrName)
def getPowerFactor(self, instrName):
return self._request_resource('getPowerFactor', instrName)
def getPhasePowerFactor(self, instrName):
return self._request_resource('getPhasePowerFactor', instrName)
def getActiveEnergyExport(self, instrName):
return self._request_resource('getActiveEnergyExport', instrName)
def getActiveEnergyImport(self, instrName):
return self._request_resource('getActiveEnergyImport', instrName)
def getReactiveEnergyExport(self, instrName):
return self._request_resource('getReactiveEnergyExport', instrName)
def getReactiveEnergyImport(self, instrName):
return self._request_resource('getReactiveEnergyImport', instrName)
def isAuthenticated(self):
return self._request_resource('isAuthenticated')
def getBreakerState(self, breakerName):
return self._request_resource('getBreakerState', breakerName)
def getFrequency(self, instrName):
return self._request_resource('getFrequency', instrName)
def getBayNames(self):
return self._request_resource('getBayNames')
def getBreakerName(self, busbarName, bayName):
return self._request_resource('getBreakerName', (busbarName, bayName))
def getInstrumentNames(self, bayName):
return self._request_resource('getInstrumentNamesPerBay', bayName)
def getBusbarNames(self):
return self._request_resource('getBusbarNames')
def getInterphaseVoltage(self, instrName):
return self._request_resource('getInterphaseVoltage', instrName)
def getInterphaseVoltages(self, instrName):
return self._request_resource('getInterphaseVoltages', instrName)
def getPhaseVoltage(self, instrName):
return self._request_resource('getPhaseVoltage', instrName)
def getPhaseVoltages(self, instrName):
return self._request_resource('getPhaseVoltages', instrName)
def getVoltageImbalance(self, instrName):
return self._request_resource('getVoltageImbalance', instrName)
def getPhaseCurrent(self, instrName):
return self._request_resource('getPhaseCurrent', instrName)
def getNeutralCurrent(self, instrName):
return self._request_resource('getNeutralCurrent', instrName)
def getPhaseCurrents(self, instrName):
return self._request_resource('getPhaseCurrents', instrName)
def getCurrentImbalance(self, instrName):
return self._request_resource('getCurrentImbalance', instrName)
def authenticate(self, user, password):
return self._request_resource('authenticate', (user, password), 'put')
def logout(self):
return self._request_resource('logout', (), 'put')
def closeBreaker(self, breakerName):
return self._request_resource('closeBreaker', breakerName, 'put')
def openBreaker(self, breakerName):
return self._request_resource('openBreaker', (breakerName), 'put')
@staticmethod
def getAvailableSwitchBoards():
return list(SwitchBoard.__SWITCH_BOARDS.keys())

View File

@ -0,0 +1,36 @@
from ..core.SyslabUnit import SyslabUnit
class WindTurbine(SyslabUnit):
__TURBINES = {
'gaia1': ('syslab-03.syslab.dk', '8080', 'gaia1'),
'simlab-01': ('192.168.0.101', '8080', 'gaia1'),
'vgaia1': ('simlab-03', '8080', 'gaia1'),
}
def __init__(self, which=None, host=None, port=None, unitname=None):
baseurl = 'http://{host}:{port}/typebased_WebService_WTGS/GaiaWindTurbineWebService/{unit_name}/'
super().__init__(
baseurl=baseurl,
which=which,
units=self.__TURBINES,
host=host,
port=port,
unit_name=unitname,
unit_type="WindTurbine")
def getGeneratorName(self):
return self._request_resource('getGeneratorName')
def getName(self):
return self.getGeneratorName()
def getActivePower(self):
return self._request_resource('getActivePower')
def getReactivePower(self):
return self._request_resource('getReactivePower')
def getWindspeed(self):
return self._request_resource('getWindspeedOutsideNacelle')

View File

@ -0,0 +1,10 @@
from .Battery import Battery
from .DieselGenerator import DieselGenerator
from .Dumpload import Dumpload
from .HeatSwitchBoard import HeatSwitchBoard
from .MeteoMast import MeteoMast
from .Photovoltaics import Photovoltaics
from .SwitchBoard import SwitchBoard
from .WindTurbine import WindTurbine
from .B2BConverter import B2BConverter
from .EVSE import EVSE

View File

@ -0,0 +1,47 @@
class FlexHouse_real():
def __init__(self):
pass
def setActivePower(self, power_reference):
if power_reference < -10:
power_reference = -10
if power_reference > 0:
raise ValueError("Positive power means production")
self.__setValue('flexHousePowerRef_kW',-power_reference)
def getTemperature(self):
return self.__getValue('flexHouseTemperature_C')
def getActivePower(self):
return self.__getValue('flexHousePower_kW')
def __getValue(self, key):
from requests import get, auth
import re
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
if key is '': raise TypeError('Key should be an empty string')
url = 'http://whiteboard.syslab.dk/wbget.php'
r = get(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
entries = r.text.split('\n')
result = None
for entry in entries:
entry = entry.rstrip().lstrip()
g = re.match('SYSLAB@(\d+):{0}=(.+);'.format(key), entry)
if g is None:
continue
else:
g = g.groups()
result = g[1]
break
return result
def __setValue(selv, key, value):
from requests import post, auth
url = '{0}{1}?source=SYSLAB&{2}={3}'.format('http://whiteboard.syslab.dk/', 'wbset.php', key, str(value))
post(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))

View File

@ -0,0 +1,51 @@
from ..physical.Dumpload import Dumpload
from ..virtual.MetMast import MetMast
from datetime import datetime
class FlexHouse_sim(Dumpload):
def __init__(self, which='localhost'):
super().__init__(which)
self.internal_temperature = 20 # Celsius
self.time = datetime.now()
self.mt = MetMast()
# The variables below are only relevant for implementing an advanced house model.
#self.__Th = 20 # internal heater state
#self.__Te = 19 # envelope state
#self.__Aw = 14.351 # m^2
#self.__Ce = 4.741 # kWh/C
#self.__Ch = 0.00225 # kWh/Cst
#self.__Ci = 2.555 # kWh/C
#self.__Rea = 3.265 # C/kW
#self.__Rie = 0.817 # C/kW
#self.__Ria = 37.005 # C/kW
#self.__Rih = 140.44 # C/kW
def setPowerConsumption(self, power_reference):
if power_reference > 10:
self.startLoad()
power_reference = 10.0
if power_reference < 0:
raise ValueError("Negative power means production")
if power_reference == 0:
self.stopLoad()
self.setPowerSetPoint(power_reference)
def step_sim(self):
time_now = datetime.now()
time_delta = (time_now - self.time).total_seconds()
T_ambient = self.mt.getTemperature()
irradiance = self.mt.getInsolation()/1000
# Below is a simple house model. A more complex one can be implemented
self.internal_temperature += 0.5*(self.getActivePower().value/3600*time_delta) - 0.00025*((self.internal_temperature-T_ambient)*time_delta) + 0.001*irradiance*time_delta
#self.__Th = 20 # internal heater state
#self.__Te = 19 # envelope state
self.time = time_now
def getTemperature(self):
self.step_sim()
return self.internal_temperature

View File

@ -0,0 +1,52 @@
from ..physical.Battery import Battery
from datetime import datetime
class FlexHouse_sim_batt(Battery):
def __init__(self, which='localhost'):
super().__init__(which)
self.internal_temperature = 20 # Celsius
self.time = datetime.now()
self.loss = 1 # kW
if self.getRemainingFloodTime() == 300:
#print('Battery is not started. I will initiate start sequence, please retry in 5 minutes')
self.startBattery()
raise SystemError('Battery is not started. I will initiate start sequence, please retry in 5 minutes')
if (self.getRemainingFloodTime() > 1) and (self.getRemainingFloodTime() < 300):
#print('Battery is not ready yet')
raise SystemError('Battery is not ready yet, please wait {} seconds'.format(self.getRemainingFloodTime()))
# The variables below are only relevant for implementing and advanced house model.
#self.__Th = 20 # internal heater state
#self.__Te = 19 # envelope state
#self.__Aw = 14.351 # m^2
#self.__Ce = 4.741 # kWh/C
#self.__Ch = 0.00225 # kWh/C
#self.__Ci = 2.555 # kWh/C
#self.__Rea = 3.265 # C/kW
#self.__Rie = 0.817 # C/kW
#self.__Ria = 37.005 # C/kW
#self.__Rih = 140.44 # C/kW
def setPowerConsumption(self, power_reference):
if power_reference > 10:
power_reference = 10
if power_reference < 0:
raise ValueError("Negative power means production")
self.setActivePower(power_reference)
def step_sim(self):
time_now = datetime.now()
time_delta = (time_now - self.time).total_seconds()
# Below is a simple house model. A more complex one can be implemented
self.internal_temperature += 0.5*(self.getActivePower().value/3600*time_delta) - 2*(self.loss/3600*time_delta)
#self.__Th = 20 # internal heater state
#self.__Te = 19 # envelope state
self.time = time_now
def getTemperature(self):
self.step_sim()
return self.internal_temperature

View File

@ -0,0 +1,40 @@
class MetMast():
def __init__(self):
pass
def getTemperature(self):
return self.__getValue('OutsideTemperature')
def getInsolation(self):
return self.__getValue('Insolation')
def getWindDirection(self):
return self.__getValue('WindDirection')
def getWindSpeed(self):
return self.__getValue('WindSpeed')
def __getValue(self, key):
from requests import get, auth
import re
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
assert key is not '', 'Key should not be an empty string'
url = 'http://whiteboard.syslab.dk/wbget.php'
r = get(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
entries = r.text.split('\n')
result = None
for entry in entries:
entry = entry.rstrip().lstrip()
g = re.match('SYSLAB@(\d+):{0}=(.+);'.format(key), entry)
if g is None:
continue
else:
g = g.groups()
result = g[1]
break
return result

View File

@ -0,0 +1,28 @@
from ..physical.Dumpload import Dumpload
from datetime import datetime
class WaterBoiler(Dumpload):
def __init__(self, which='localhost'):
super().__init__(which)
self.energy_state = 10 # kWh
self.energy_max = 15
self.time = datetime.now()
self.loss = 1 # kW
def step_sim(self):
time_now = datetime.now()
time_delta = (time_now - self.time).total_seconds()
power = self.getActivePower().value
self.energy_state += (power - self.loss)/3600*time_delta
self.time = time_now
def getSOC(self):
self.step_sim()
if self.energy_state > self.energy_max:
self.energy_state = self.energy_max
elif self.energy_state < 0:
self.energy_state = 0.0
return self.energy_state

View File

@ -0,0 +1 @@
from . import *

View File

@ -0,0 +1,143 @@
from syslab.whiteboard.WhiteBoardEntry import WhiteBoardEntry
class CommModule():
__BASE_URL = 'http://whiteboard.syslab.dk/'
def __init__(self, namespace):
if not type(namespace) is str or len(namespace) == 0: raise TypeError('Namespace should be an non-empty string, found "{0}"'.format(type(namespace)))
self.__namespace = namespace
self.__entries = {}
def appendValue(self, key, value):
values = self.getList(key)
if values == None:
self.publishToWhiteBoardServer(key, '[{0}]'.format(str(value)))
else:
values.append(value)
self.publishList(key, values)
def getList(self, key):
from ast import literal_eval
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
entry = self.getFromWhiteBoardServer(key)
values = None
if entry is not None:
values_str = str(entry.value)
values_str = values_str.replace('[','["').replace(']','"]'.replace(',','","'))
values = literal_eval(values_str)
if type(values) is not list:
values = [values, ]
return values
def getAllEntries(self):
return self.__entries
def update(self):
from requests import get, auth
import re
url = CommModule.__BASE_URL + 'wbget.php'
r = get(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
entries = r.text.split('\n')
result = {}
for entry in entries:
entry = entry.rstrip().lstrip()
g = re.match('SYSLAB@(\d+):{0}::(\w+)=(.+);'.format(self.__namespace), entry)
if g is None:
continue
else:
g = g.groups()
self.__entries[g[1]] = WhiteBoardEntry(g[1], g[2], g[0])
def printEntries(self):
if len(self.__entries)==0:
print('No WhiteBoard entries found for namespace: {0}'.format(self.__namespace))
return
for e in self.__entries:
print(e)
def publishList(self, key, values):
from requests import post, auth
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
if not type(values) is list: raise TypeError('Values should be represented by a list'.format(type(values)))
values_str = "[{0}]".format(",".join(map(str,values)))
url = '{0}{1}?source=SYSLAB&{2}::{3}={4}'.format(CommModule.__BASE_URL, 'wbset.php', self.__namespace, str(key), values_str)
post(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
def getFromWhiteBoardServer(self, key):
"""
Go in and read/write the value from a key from the whiteboard. The value
should use decimal dot, not comma!
"""
from requests import get, auth
import re
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
if key is '': raise TypeError('Key should be an empty string')
url = CommModule.__BASE_URL+'wbget.php'
r = get(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
entries = r.text.split('\n')
result = None
for entry in entries:
entry = entry.rstrip().lstrip()
g = re.match('SYSLAB@(\d+):{0}::{1}=(.+);'.format(self.__namespace, key), entry)
if g is None:
continue
else:
g = g.groups()
result = WhiteBoardEntry(key, g[1], g[0])
break
return result
def publishToWhiteBoardServer(self, key, value):
from requests import post, auth
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
if type(value) is list or type(value) is dict or type(value) is tuple: raise TypeError('This function only supports single values, found {0}'.format(type(value)))
url = '{0}{1}?source=SYSLAB&{2}::{3}={4}'.format(CommModule.__BASE_URL, 'wbset.php', self.__namespace, str(key), str(value))
post(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
def getPoolKeys(self):
from requests import get, auth
import re
url = CommModule.__BASE_URL+'wbget.php'
r = get(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
entries = r.text.split('\n')
result = {}
for entry in entries:
entry = entry.rstrip().lstrip()
g = re.match('SYSLAB@(\d+):{0}::(\w+)=(.+)'.format(self.__namespace), entry)
if g is None:
continue
else:
g = g.groups()
result[g[1]] = g[2]
return result
def clearKey(self, key):
from requests import post, auth
if not type(key) is str: raise TypeError('Key should be a string, found {0}'.format(type(key)))
url = '{0}{1}?source=SYSLAB&key={2}::{3}'.format(CommModule.__BASE_URL, 'wbclean.php', self.__namespace, str(key))
post(url, auth=auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
def clearAllKeys(self):
entries = self.getPoolKeys()
for key in entries.keys():
self.clearKey(key)

View File

@ -0,0 +1,21 @@
class WhiteBoardEntry:
def __init__(self, key, value, time):
self.__key = key
self.__value = value
self.__time = int(time)
@property
def key(self):
return self.__key
@property
def value(self):
return self.__value
@property
def time(self):
return self.__time
def __str__(self):
from datetime import datetime
return 'WhiteBoardEntry: {0} -> {1} (@time: {2})'.format(self.key, self.value, datetime.fromtimestamp(self.time))

View File

@ -0,0 +1,23 @@
from bs4 import BeautifulSoup
import requests
def getFromWhiteBoardServer(key):
"""
Go in and read/write the value from a key from the whiteboard. The value
should use decimal dot, not comma!
"""
url = 'http://whiteboard.syslab.dk/wbget.php?mode=html'
r = requests.get(url, auth=requests.auth.HTTPBasicAuth('twinPV99', 'twinPV99'))
data = r.text
print(data)
soup = BeautifulSoup(data)
table = soup.find('table')
key = table.find('td', text=key)
value = key.findNext('td')
return value.text
def publishToWhiteBoardServer(key, value):
url = 'http://whiteboard.syslab.dk/wbset.php?source=SYSLAB&' + key + '=' + str(value)
r = requests.post(url, auth=requests.auth.HTTPBasicAuth('twinPV99', 'twinPV99'))

View File

@ -0,0 +1,3 @@
from .CommModule import CommModule
from .WhiteBoardEntry import WhiteBoardEntry
from .Whiteboard import publishToWhiteBoardServer, getFromWhiteBoardServer

View File

View File

View File

@ -1,26 +0,0 @@
import sys
import zmq
SERVER_ADDR = 'localhost'
SERVER_PORT = int('5556')
if len(sys.argv) > 1:
port = int(sys.argv[1])
# Socket to talk to server
context = zmq.Context()
socket = context.socket(zmq.SUB)
print(f"Collecting updates from time server at tcp://localhost:{SERVER_PORT}")
socket.connect(f"tcp://{SERVER_ADDR}:{SERVER_PORT}")
topicfilter = 'POST'
socket.setsockopt_string(zmq.SUBSCRIBE, topicfilter)
while True:
try:
incoming = socket.recv_string()
topic, message = incoming.split(';')
print(f'New post from user ' + message)
except KeyboardInterrupt:
print('Closing listener...')
break

View File

@ -1,10 +0,0 @@
from xmlrpc.client import ServerProxy
import sys
if __name__ == '__main__':
username = input('Username: ')
with ServerProxy('http://localhost:9000') as proxy:
while True:
message = input('Message: ')
remote_call = proxy.post(message, username)
print(remote_call)

View File

@ -1,29 +0,0 @@
import zmq
import sys
from xmlrpc.server import SimpleXMLRPCServer
PUB_PORT = int('5556')
RPC_PORT = int('9000')
POST_TOPIC = 'POST'
if len(sys.argv) > 1:
PUB_PORT = int(sys.argv[1])
context = zmq.Context()
socket = context.socket(zmq.PUB)
socket.bind(f"tcp://*:{PUB_PORT}")
def post(msg, user):
socket.send_string(f"{POST_TOPIC};{user}: {msg}")
print(f"Published post {user}: {msg}")
return 'Success'
if __name__ == "__main__":
server = SimpleXMLRPCServer(('localhost', RPC_PORT), logRequests=True)
server.register_function(post, 'post')
try:
print("Use Control-C to exit")
server.serve_forever()
except KeyboardInterrupt:
print("Exiting")

5
util.py Normal file
View File

@ -0,0 +1,5 @@
def clamp(a, x, b):
"""
Restrict x to lie in the range [a, b]
"""
return max(a, min(x, b))