Compare commits
6 Commits
main
...
communicat
| Author | SHA1 | Date |
|---|---|---|
|
|
5c488235d3 | |
|
|
7685c36677 | |
|
|
1044d6224a | |
|
|
9aec9332fc | |
|
|
e00a4587f9 | |
|
|
16d4b96c55 |
Binary file not shown.
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 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
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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())
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
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)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Makes folder visible for git.
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1 +0,0 @@
|
||||||
Makes folder visible for git.
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
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)
|
|
||||||
139
demo_plotter.py
139
demo_plotter.py
|
|
@ -1,139 +0,0 @@
|
||||||
import pandas as pd
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# 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_pivot = df.pivot_table(values='value', columns='unit', index='time')
|
|
||||||
|
|
||||||
|
|
||||||
# Plot the data. Note, that the data will mostly not be plotted with lines.
|
|
||||||
plt.ion() # Turn interactive mode on
|
|
||||||
plt.figure()
|
|
||||||
ax1 = plt.subplot(211) # Make two separate figures
|
|
||||||
ax2 = plt.subplot(212)
|
|
||||||
df_pivot[[c for c in df_pivot.columns if "_p" in c]].plot(marker='.', ax=ax1, linewidth=3)
|
|
||||||
df_pivot[[c for c in df_pivot.columns if "_q" in c]].plot(marker='.', ax=ax2, linewidth=3)
|
|
||||||
plt.show(block=True)
|
|
||||||
|
|
||||||
|
|
||||||
## TODO Q1: Your code here
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## TODO Q2:
|
|
||||||
# Convert time column (index) of df_pivot to datetime
|
|
||||||
# TODO Your code here
|
|
||||||
# Hint1: You can use pandas to_numeric() to prepare the index for pandas to_datetime function
|
|
||||||
# Hint2: Remember to define the unit within pandas to_datetime function
|
|
||||||
|
|
||||||
# Resample the data
|
|
||||||
# TODO Your code here
|
|
||||||
|
|
||||||
|
|
||||||
# Interpolate the measurements
|
|
||||||
# TODO Your code here
|
|
||||||
# Hint: For part two of the exercise ("collecting fresh data") the nan rows after a setpoint
|
|
||||||
# in the recorded step function should be filled with the value of the setpoint until the row of the next setpoint is reached
|
|
||||||
# You can use the df.fillna(method="ffill") function for that purpose. However, the measurements should still be interpolated!
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Plot the resampled data
|
|
||||||
# TODO Your code here
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## TODO Q3: Your code here
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## TODO Q4: Your code here
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Part two: "Collecting fresh data"
|
|
||||||
|
|
||||||
# Hint 1: You can build up on the "read_and_plot_data.py" from day 2
|
|
||||||
# Hint 2: Yoy may want to store your response metric functions from day 2 in the "util.py" and import all of them with
|
|
||||||
# "from util import *"
|
|
||||||
|
|
||||||
if use_setpoint_log:
|
|
||||||
|
|
||||||
# Add a column to df_pivot containing the reference/target signal
|
|
||||||
# TODO your code here
|
|
||||||
|
|
||||||
# Loop over all steps and extract T_1, T_2 and the step size
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for idx in range(0, len(sp_data)-1):
|
|
||||||
label = f"Step_{sp_data[idx]['value']}kW"
|
|
||||||
|
|
||||||
# Extract T_1 and T_2 from the setpoint JSON
|
|
||||||
# TODO your code here
|
|
||||||
|
|
||||||
|
|
||||||
# Change timestamp format
|
|
||||||
T_1 = pd.to_datetime(pd.to_numeric(T_1), unit="s").round("0.1S")
|
|
||||||
T_2 = pd.to_datetime(pd.to_numeric(T_2), unit="s").round("0.1S")
|
|
||||||
|
|
||||||
# To ensure we are not considering values of the next load step
|
|
||||||
T_2 = T_2 - timedelta(seconds=0.2)
|
|
||||||
|
|
||||||
|
|
||||||
# define measured output y and target setpoint r
|
|
||||||
# TODO your code here
|
|
||||||
|
|
||||||
# Derive step direction from the setpoint data
|
|
||||||
if ...: # TODO your code here
|
|
||||||
Positive_step = True
|
|
||||||
else:
|
|
||||||
Positive_step = False
|
|
||||||
|
|
||||||
# Collect response metrics results
|
|
||||||
results[label] = {
|
|
||||||
# TODO your code here
|
|
||||||
}
|
|
||||||
|
|
||||||
pd.DataFrame.from_dict(results).plot(kind='bar')
|
|
||||||
plt.title("Metrics")
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig('data/test_metrics'+MEAS_LOG_FILE[-10:]+'.png')
|
|
||||||
plt.show(block=True)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
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}")
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
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}")
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
beautifulsoup4==4.12.3
|
aiohttp==3.9.5
|
||||||
certifi==2024.6.2
|
aiosignal==1.3.1
|
||||||
charset-normalizer==3.3.2
|
attrs==23.2.0
|
||||||
|
frozenlist==1.4.1
|
||||||
greenlet==3.0.3
|
greenlet==3.0.3
|
||||||
idna==3.7
|
idna==3.7
|
||||||
msgpack==1.0.8
|
msgpack==1.0.8
|
||||||
|
multidict==6.0.5
|
||||||
|
parse==1.20.2
|
||||||
pynvim==0.5.0
|
pynvim==0.5.0
|
||||||
requests==2.32.3
|
pyzmq==26.0.3
|
||||||
soupsieve==2.5
|
yarl==1.9.4
|
||||||
# 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.
Binary file not shown.
|
|
@ -0,0 +1,186 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
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))
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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")
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
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")
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
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")
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
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")
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
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
|
||||||
|
|
@ -1,109 +0,0 @@
|
||||||
# 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/
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
requests >= 2.18
|
|
||||||
beautifulsoup4 >= 3.7
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
[flake8]
|
|
||||||
ignore =
|
|
||||||
max-line-length = 79
|
|
||||||
max-complexity = 11
|
|
||||||
|
|
||||||
[pytest]
|
|
||||||
addopts = --doctest-glob="*.rst"
|
|
||||||
|
|
||||||
[wheel]
|
|
||||||
universal = True
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
# -*- 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
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
###############################################################
|
|
||||||
# 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)
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
#from BroadcastLogger import BroadcastLogSender
|
|
||||||
#from LogUtils import setup_udp_logger
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
###############################################################
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
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("(\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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from . import *
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
# TODO: Implement
|
|
||||||
class CommonDeviceConfig:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# TODO: Add Processing here
|
|
||||||
|
|
||||||
|
|
||||||
class CompositeStatus:
|
|
||||||
pass
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
|
|
||||||
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
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
__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
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
@ -1,258 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
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")
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
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())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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')
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
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'))
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
from . import *
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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))
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
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'))
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
from .CommModule import CommModule
|
|
||||||
from .WhiteBoardEntry import WhiteBoardEntry
|
|
||||||
from .Whiteboard import publishToWhiteBoardServer, getFromWhiteBoardServer
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
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
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
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)
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
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")
|
||||||
Loading…
Reference in New Issue