Skip to content

Commit 2a1d32f

Browse files
committed
Added an interactive mode to allow you to run the simulation with different values and get the results back
1 parent 4e21826 commit 2a1d32f

File tree

8 files changed

+587
-339
lines changed

8 files changed

+587
-339
lines changed

divert_sim/interactive.html

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>OpenEVSE Solar Divert Simulations</title>
6+
<script type="text/javascript" src="https://canvasjs.com/assets/script/jquery-1.11.1.min.js"></script>
7+
<script type="text/javascript" src="https://canvasjs.com/assets/script/canvasjs.min.js"></script>
8+
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
9+
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" />
10+
<script type="text/javascript" src="simulations.js"></script>
11+
<script type="text/javascript">
12+
var profiles = [
13+
"master",
14+
"interactive"
15+
];
16+
17+
function run_simulation() {
18+
$.post("simulation", JSON.stringify({
19+
"divert_PV_ratio": parseFloat($("#divert_PV_ratio").val()),
20+
"divert_attack_smoothing_time": parseInt($("#divert_attack_smoothing_time").val()),
21+
"divert_decay_smoothing_time": parseInt($("#divert_decay_smoothing_time").val()),
22+
"divert_min_charge_time": parseInt($("#divert_min_charge_time").val())
23+
}), () => {
24+
init_summary(profiles);
25+
loadSummary("output/summary_master.csv", () => {
26+
loadSummary("output/summary_interactive.csv", () => {
27+
generate_summary_table(profiles);
28+
for (const dataset of datasets) {
29+
var id = dataset.id + "_interactive";
30+
loadChart(id, "output/"+ id + ".csv", dataset.title + " (interactive)", dataset.class);
31+
}
32+
}, "interactive");
33+
});
34+
});
35+
}
36+
</script>
37+
<style type="text/css" media="print">
38+
.solar,.gridie
39+
{
40+
page-break-inside: avoid;
41+
}
42+
</style>
43+
</head>
44+
45+
<body>
46+
<h1>OpenEVSE Solar Divert Simulations</h1>
47+
48+
<a href="view.html">Test Results</a><br/>
49+
50+
<label for="divert_PV_ratio">divert_PV_ratio</label>
51+
<input type="text" id="divert_PV_ratio" value="1.1"><br/>
52+
<label for="divert_attack_smoothing_time">divert_attack_smoothing_time</label>
53+
<input type="text" id="divert_attack_smoothing_time" value="20"><br/>
54+
<label for="divert_decay_smoothing_time">divert_decay_smoothing_time</label>
55+
<input type="text" id="divert_decay_smoothing_time" value="200"><br/>
56+
<label for="divert_min_charge_time">divert_min_charge_time</label>
57+
<input type="text" id="divert_min_charge_time" value="600"><br/>
58+
59+
<button onclick="run_simulation()">Run Simulation</button>
60+
61+
<h2>Summary</h2>
62+
<div id="summary_table">
63+
</div>
64+
65+
<script type="text/javascript">
66+
67+
for (const dataset of datasets) {
68+
var id = generate_chart(dataset, "master");;
69+
loadChart(id, "output/"+ id + ".csv", dataset.title + " (master)", dataset.class);
70+
generate_chart(dataset, "interactive");
71+
}
72+
//<div id="day1_default" style="width:100%; height:300px;" class="solar" csv="output/day1_default.csv" title="Day 1"></div>
73+
74+
</script>
75+
76+
</body>
77+
78+
</html>

divert_sim/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
pytest
2+
simplejson

divert_sim/run_simulations.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Test the divert_sim process"""
2+
3+
# pylint: disable=line-too-long
4+
5+
from os import path
6+
import os
7+
from subprocess import PIPE, Popen
8+
from datetime import datetime
9+
10+
OPENEVSE_STATE_STARTING = 0
11+
OPENEVSE_STATE_NOT_CONNECTED = 1
12+
OPENEVSE_STATE_CONNECTED = 2
13+
OPENEVSE_STATE_CHARGING = 3
14+
OPENEVSE_STATE_VENT_REQUIRED = 4
15+
OPENEVSE_STATE_DIODE_CHECK_FAILED = 5
16+
OPENEVSE_STATE_GFI_FAULT = 6
17+
OPENEVSE_STATE_NO_EARTH_GROUND = 7
18+
OPENEVSE_STATE_STUCK_RELAY = 8
19+
OPENEVSE_STATE_GFI_SELF_TEST_FAILED = 9
20+
OPENEVSE_STATE_OVER_TEMPERATURE = 0
21+
OPENEVSE_STATE_OVER_CURRENT = 1
22+
OPENEVSE_STATE_SLEEPING = 4
23+
OPENEVSE_STATE_DISABLED = 5
24+
25+
KWH_ROUNDING = 2
26+
27+
summary_filename = 'summary.csv'
28+
29+
def setup_summary(postfix: str = ''):
30+
global summary_filename
31+
"""Create the output directory"""
32+
print("Setting up test environment")
33+
if not path.exists('output'):
34+
os.mkdir('output')
35+
summary_filename = 'summary'+postfix+'.csv'
36+
with open(path.join('output', summary_filename), 'w', encoding="utf-8") as summary_file:
37+
summary_file.write('"Dataset","Config","Total Solar (kWh)","Total EV Charge (kWh)","Charge from solar (kWh)","Charge from grid (kWh)","Number of charges","Min time charging","Max time charging","Total time charging"\n')
38+
39+
def run_simulation(dataset: str,
40+
output: str,
41+
config: bool = False, grid_ie_col: bool = False,
42+
solar_col: bool = False, voltage_col: bool = False,
43+
separator: str = ',', is_kw: bool = False) -> None:
44+
"""Run the divert_sim process on the given dataset and return the results"""
45+
line_number = 0
46+
47+
last_date = None
48+
last_state = 0
49+
charge_start_date = None
50+
51+
total_solar_wh = 0
52+
total_ev_wh = 0
53+
wh_from_solar = 0
54+
wh_from_grid = 0
55+
number_of_charges = 0
56+
min_time_charging = 0
57+
max_time_charging = 0
58+
total_time_charging = 0
59+
60+
print("Testing dataset: " + dataset)
61+
62+
# Read in the dataset and pass to the divert_sim process
63+
with open(path.join('data', dataset+'.csv'), 'r', encoding="utf-8") as input_data:
64+
with open(path.join('output', output+'.csv'), 'w', encoding="utf-8") as output_data:
65+
# open the divert_sim process
66+
command = ["./divert_sim"]
67+
if config:
68+
command.append("-c")
69+
command.append(config)
70+
if grid_ie_col:
71+
command.append("-g")
72+
command.append(str(grid_ie_col))
73+
if solar_col:
74+
command.append("-s")
75+
command.append(str(solar_col))
76+
if voltage_col:
77+
command.append("-v")
78+
command.append(str(voltage_col))
79+
if separator:
80+
command.append("--sep")
81+
command.append(separator)
82+
if is_kw:
83+
command.append("--kw")
84+
85+
divert_process = Popen(command, stdin=input_data, stdout=PIPE,
86+
stderr=PIPE, universal_newlines=True)
87+
while True:
88+
output = divert_process.stdout.readline()
89+
if output == '' and divert_process.poll() is not None:
90+
break
91+
if output:
92+
output_data.write(output)
93+
line_number += 1
94+
if line_number > 1:
95+
# read in the csv line
96+
csv_line = output.split(',')
97+
date = datetime.strptime(csv_line[0], '%d/%m/%Y %H:%M:%S')
98+
solar = float(csv_line[1])
99+
# grid_ie = float(csv_line[2])
100+
# pilot = int(csv_line[3])
101+
charge_power = float(csv_line[4])
102+
# min_charge_power = float(csv_line[5])
103+
state = int(csv_line[6])
104+
# smoothed_available = float(csv_line[7])
105+
106+
if last_date is not None:
107+
# Get the difference between this date and last date
108+
diff = date - last_date
109+
110+
hours = diff.seconds / 3600
111+
112+
total_solar_wh += solar * hours
113+
ev_wh = charge_power * hours
114+
total_ev_wh += charge_power * hours
115+
charge_from_solar_wh = min(solar, charge_power) * hours
116+
wh_from_solar += charge_from_solar_wh
117+
wh_from_grid += ev_wh - charge_from_solar_wh
118+
119+
if state == OPENEVSE_STATE_CHARGING and last_state != OPENEVSE_STATE_CHARGING:
120+
number_of_charges += 1
121+
charge_start_date = date
122+
123+
if state != OPENEVSE_STATE_CHARGING and last_state == OPENEVSE_STATE_CHARGING:
124+
this_session_time_charging = (date - charge_start_date).seconds
125+
total_time_charging += this_session_time_charging
126+
if min_time_charging == 0 or this_session_time_charging < min_time_charging:
127+
min_time_charging = this_session_time_charging
128+
if this_session_time_charging > max_time_charging:
129+
max_time_charging = this_session_time_charging
130+
131+
last_date = date
132+
last_state = state
133+
134+
solar_kwh=total_solar_wh / 1000
135+
ev_kwh=total_ev_wh / 1000
136+
kwh_from_solar=wh_from_solar / 1000
137+
kwh_from_grid=wh_from_grid / 1000
138+
139+
if config is False or config.startswith('{'):
140+
config = "Default"
141+
142+
with open(path.join('output', summary_filename), 'a', encoding="utf-8") as summary_file:
143+
summary_file.write(f'"{dataset}","{config}",{solar_kwh},{ev_kwh},{kwh_from_solar},{kwh_from_grid},{number_of_charges},{min_time_charging},{max_time_charging},{total_time_charging}\n')
144+
145+
146+
return (round(solar_kwh, KWH_ROUNDING),
147+
round(ev_kwh, KWH_ROUNDING),
148+
round(kwh_from_solar, KWH_ROUNDING),
149+
round(kwh_from_grid, KWH_ROUNDING),
150+
number_of_charges,
151+
min_time_charging,
152+
max_time_charging,
153+
total_time_charging)

divert_sim/server.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""
2+
Simple HTTP server to run dynamic simulations and view results.
3+
"""
4+
5+
import http.server
6+
import socketserver
7+
import simplejson as json
8+
9+
from run_simulations import run_simulation, setup_summary
10+
11+
class MyHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
12+
"""
13+
This class will handles any incoming request from the browser
14+
"""
15+
def do_GET(self):
16+
if self.path == '/':
17+
self.path = 'view.html'
18+
return http.server.SimpleHTTPRequestHandler.do_GET(self)
19+
20+
def do_POST(self):
21+
content_length = int(self.headers['Content-Length']) # <--- Gets the size of data
22+
post_data = self.rfile.read(content_length) # <--- Gets the data itself
23+
self.send_response(200)
24+
self.end_headers()
25+
26+
try:
27+
config = json.loads(post_data)
28+
except ValueError as e:
29+
config = {}
30+
print("{}".format(config))
31+
32+
# Run the simulation
33+
setup_summary('_interactive')
34+
run_simulation('almostperfect', 'almostperfect_interactive',
35+
config=json.dumps(config))
36+
37+
run_simulation('CloudyMorning', 'CloudyMorning_interactive',
38+
config=json.dumps(config))
39+
40+
run_simulation('day1', 'day1_interactive',
41+
config=json.dumps(config))
42+
43+
run_simulation('day2', 'day2_interactive',
44+
config=json.dumps(config))
45+
46+
run_simulation('day3', 'day3_interactive',
47+
config=json.dumps(config))
48+
49+
run_simulation('day1_grid_ie', 'day1_grid_ie_interactive',
50+
grid_ie_col=2, config=json.dumps(config))
51+
52+
run_simulation('day2_grid_ie', 'day2_grid_ie_interactive',
53+
grid_ie_col=2, config=json.dumps(config))
54+
55+
run_simulation('day3_grid_ie', 'day3_grid_ie_interactive',
56+
grid_ie_col=2, config=json.dumps(config))
57+
58+
run_simulation('solar-vrms', 'solar-vrms_interactive',
59+
voltage_col=2, config=json.dumps(config))
60+
61+
run_simulation('Energy_and_Power_Day_2020-03-22', 'Energy_and_Power_Day_2020-03-22_interactive',
62+
separator=';', is_kw=True, config=json.dumps(config))
63+
64+
run_simulation('Energy_and_Power_Day_2020-03-31', 'Energy_and_Power_Day_2020-03-31_interactive',
65+
separator=';', is_kw=True, config=json.dumps(config))
66+
67+
run_simulation('Energy_and_Power_Day_2020-04-01', 'Energy_and_Power_Day_2020-04-01_interactive',
68+
separator=';', is_kw=True, config=json.dumps(config))
69+
70+
71+
self.wfile.write("OK".encode('utf-8'))
72+
73+
74+
# Create an object of the above class
75+
handler_object = MyHttpRequestHandler
76+
77+
PORT = 8000
78+
my_server = socketserver.TCPServer(("", PORT), handler_object)
79+
80+
# Start the server
81+
print("Server started at localhost:" + str(PORT))
82+
my_server.serve_forever()

0 commit comments

Comments
 (0)