initial commit

This commit is contained in:
Milan Toman
2018-07-02 10:59:57 +02:00
commit df2c704270
64 changed files with 28611 additions and 0 deletions
Executable
+2
View File
@@ -0,0 +1,2 @@
Absolute web path throughout the project
Graphs share JS variables, separate them.
Executable
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/python3
import os
import cherrypy
import OpenSSL
import sys
PATH = os.path.abspath(os.path.dirname(__file__))
_SCRIPT_PATH = os.path.dirname(sys.argv[0])
sys.path.append(_SCRIPT_PATH)
import voltage
import weather
import dynamic
import status
server_config={
'server.socket_host': '0.0.0.0',
'server.socket_port':443,
'server.ssl_module':'builtin',
'server.ssl_certificate':'/home/www/.cert/fullchain1.pem',
'server.ssl_private_key':'/home/www/.cert/privkey1.pem'
}
cherrypy.config.update(server_config)
if __name__ == '__main__':
conf = {
'/': {
'tools.sessions.on': True,
'tools.staticdir.root': os.path.abspath(_SCRIPT_PATH + '/')
},
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': './static'
},
'/data': {
'tools.staticdir.on': False,
'tools.staticdir.dir': './dynamic'
}
}
cherrypy.tree.mount(voltage.EnergyInfo(), "/", conf)
cherrypy.tree.mount(weather.WeatherInfo(), "/weather", conf)
cherrypy.tree.mount(status.StatusInfo(), "/status", conf)
cherrypy.tree.mount(dynamic.DynamicData(), "/data", conf)
cherrypy.engine.start()
cherrypy.engine.block()
Executable
+497
View File
@@ -0,0 +1,497 @@
#!/usr/bin/python3
import os
import sys
import cherrypy
import influxdb
import time
import datetime
import json
from cherrypy.lib.httputil import parse_query_string
# Universal variables
_SCRIPT_PATH = os.path.dirname(sys.argv[0])
influx_host = 'localhost'
influx_port = 8086
influx_user = 'pi'
influx_pwd = 'freedavis'
influx_db = 'voltage'
influx_weather_db = 'weather'
influx_status_db = 'status'
variables_known = ["range", "granularity", "start", "end"]
default_variables = {"range": "1h", "granularity": "30s", "end": "1s"}
class DynamicData(object):
def __init__(self):
self.influx_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_db
)
self.influx_weather_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_weather_db
)
self.influx_status_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_status_db
)
@cherrypy.expose
def index(self):
return "Index, MOFO"
def check_GET(self, arguments):
''' No other purpose than to make it more secure and only process the
values that are meant to be processed. Hardcoded on top, mate!
sets:
list(): key values of those which should be processed, merged
with deault values
'''
self.q = eval(str(arguments))
keys_to_process = {
key:self.q[key] for key in self.q.keys() if key in variables_known}
resulting_variables = default_variables.copy()
resulting_variables.update(keys_to_process)
self.q = resulting_variables
@cherrypy.expose
def solar_monitor(self, **kwargs):
'''
Function to get solar readings from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time, V_solar, I_solar, P_solar
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
query1 = "SELECT mean(V_solar) as Usol, mean(I_solar) as Isol, "
query2 = "mean(V_array) as Varr, mean(I_consumption) as Icons FROM voltage "
query3 = "WHERE time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
query4 = "ORDER BY time DESC"
query = "{} {} {} {}".format(query1, query2, query3, query4)
measures = self.influx_client.query(query)
# Let's get the data from DB
header = "time,V_solar,I_solar,P_solar,P_cons\n"
yield header
for datapoint in measures["voltage"]:
tm = str(datapoint["time"]).strip()
solar_voltage = str(datapoint["Usol"]).strip()
solar_current = str(datapoint["Isol"]).strip()
array_voltage = str(datapoint["Varr"]).strip()
consumption_current = str(datapoint["Icons"]).strip()
if solar_voltage != 'None' and solar_current != 'None' \
and array_voltage != 'None' and consumption_current != 'None':
solar_voltage = float(solar_voltage) / 1000.00
solar_current = float(solar_current) / 1000.00
array_voltage = float(array_voltage) / 1000.00
consumption_current = float(consumption_current) / 1000.00
else:
solar_voltage = 0.00
solar_current = 0.00
array_voltage = 0.00
consumption_current = 0.00
# The 8W is the approximate internal consumption of the mppt controller ~ 0.15A
# This value was removed, No idea why it appeared there in the first place.
solar_power = round(solar_voltage * solar_current, 2)
consumption_power = round(array_voltage * consumption_current, 2)
yield "{},{},{},{},{}\n".format(tm, solar_voltage,
solar_current, solar_power,
consumption_power)
@cherrypy.expose
def wind_monitor(self, **kwargs):
'''
Function to get wind value readings from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time, Speed, Gusts, Direction
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
speed_q1 = "SELECT mean(value) as w_speed FROM wind"
gust_q1 = "SELECT mean(value) as w_gust FROM wind"
direction_q1 = "SELECT mean(value) as w_dir FROM wind"
speed_q2 = "WHERE time > NOW() - {} AND time < NOW() - {} AND type = 'speed' GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
gust_q2 = "WHERE time > NOW() - {} AND time < NOW() - {} AND type = 'windgust' GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
direction_q2 = "WHERE time > NOW() - {} AND time < NOW() - {} AND type = 'direction' GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
q3 = "ORDER BY time DESC"
speed_query = "{} {} {}".format(speed_q1, speed_q2, q3)
gust_query = "{} {} {}".format(gust_q1, gust_q2, q3)
direction_query = "{} {} {}".format(direction_q1, direction_q2, q3)
rs_speed = self.influx_weather_client.query(speed_query)
rs_gust = self.influx_weather_client.query(gust_query)
rs_direction = self.influx_weather_client.query(direction_query)
# Let's get the data from DB
header = "time,Speed,Gusts,Direction\n"
yield header
for speed, gust, direction in zip(rs_speed['wind'], rs_gust['wind'], rs_direction['wind']):
tm_speed = str(speed["time"]).strip()
tm_gust = str(gust["time"]).strip()
tm_direction = str(direction["time"]).strip()
speed_value = str(speed["w_speed"]).strip()
gust_value = str(gust["w_gust"]).strip()
direction_value = str(direction["w_dir"]).strip()
#if tm_speed == tm_gust and tm_speed == tm_direction:
#tm = strptime(speed["time"]).strip(), "%Y-%m-%dT%H:%M:%SZ")
yield "{},{},{},{}\n".format(tm_speed, speed_value, gust_value, direction_value)
@cherrypy.expose
def temphumi_monitor(self, **kwargs):
'''
Function to get temperature and humidity readings from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time,
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
temp_q1 = "SELECT mean(temperature) as temp FROM temphumi"
hum_q1 = "SELECT mean(humidity) as hum FROM temphumi"
in_q2 = "WHERE time > NOW() - {} AND time < NOW() - {} AND type = 'internal' GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
out_q2 = "WHERE time > NOW() - {} AND time < NOW() - {} AND type = 'external' GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
q3 = "ORDER BY time DESC"
temp_in_query = "{} {} {}".format(temp_q1, in_q2, q3)
temp_out_query = "{} {} {}".format(temp_q1, out_q2, q3)
hum_in_query = "{} {} {}".format(hum_q1, in_q2, q3)
hum_out_query = "{} {} {}".format(hum_q1, out_q2, q3)
rs_temp_in = self.influx_weather_client.query(temp_in_query)
rs_temp_out = self.influx_weather_client.query(temp_out_query)
rs_hum_in = self.influx_weather_client.query(hum_in_query)
rs_hum_out = self.influx_weather_client.query(hum_out_query)
# Let's get the data from DB
header = "time,T(ins),T(out),Humi(ins),Humi(out)\n"
yield header
for Tin, Tout, Hin, Hout in zip(rs_temp_in['temphumi'],
rs_temp_out['temphumi'],
rs_hum_in['temphumi'],
rs_hum_out['temphumi']):
tm_temp = str(Tin["time"]).strip()
temp_in_val = str(Tin["temp"]).strip()
temp_out_val = str(Tout["temp"]).strip()
hum_in_val = str(Hin["hum"]).strip()
hum_out_val = str(Hout["hum"]).strip()
#if tm_speed == tm_gust and tm_speed == tm_direction:
#tm = strptime(speed["time"]).strip(), "%Y-%m-%dT%H:%M:%SZ")
yield "{},{},{},{},{}\n".format(tm_temp, temp_in_val, temp_out_val, hum_in_val, hum_out_val)
@cherrypy.expose
def pressure_monitor(self, **kwargs):
'''
Function to get pressure readings from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time, Pressure
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
query1 = "SELECT mean(pressure) as pressure FROM temphumi"
query2 = "WHERE type = 'adjusted' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
query3 = "ORDER BY time DESC"
query = "{} {}".format(query1, query2, query3)
measures = self.influx_weather_client.query(query)
# Let's get the data from DB
header = "time,Pressure\n"
yield header
for datapoint in measures["temphumi"]:
tm = str(datapoint["time"]).strip()
pressure = str(datapoint["pressure"]).strip()
yield "{},{}\n".format(tm, pressure)
@cherrypy.expose
def solcap_monitor(self, **kwargs):
'''
Function to get solar and supercap readings from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time, Solar Irradiance, Capacitor
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
solar_query1 = "SELECT mean(voltage) as solar FROM iss"
solar_query2 = "WHERE type = 'solar' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
cap_query1 = "SELECT mean(voltage) as cap FROM iss"
cap_query2 = "WHERE type = 'capcaitor' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
query3 = "ORDER BY time DESC"
query_solar = "{} {} {}".format(solar_query1, solar_query2, query3)
query_cap = "{} {} {}".format(cap_query1, cap_query2, query3)
measures_sol = self.influx_status_client.query(query_solar)
measures_cap = self.influx_status_client.query(query_cap)
# Let's get the data from DB
header = "time,Solar,Capacitor\n"
yield header
for Sol, Cap in zip(measures_sol['iss'], measures_cap['iss']):
tm = str(Sol["time"]).strip()
try:
solar_value = float(str(Sol["solar"]).strip()) / 100
except:
solar_value = ''
try:
cap_value = float(str(Cap["cap"]).strip())
except:
cap_value = ''
yield "{},{},{}\n".format(tm, solar_value, cap_value)
@cherrypy.expose
def cpumem_monitor(self, **kwargs):
'''
Function to get cpu,mem, disk % from InfluxDB.
These parsed into a CSV
yields: csv in raw, text format
time, Cpu Mem, Disk
'''
# GET variables now set, ready to reference them
self.check_GET(kwargs)
cpu_query1 = "SELECT mean(usage) as Cpu "
mem_query1 = "SELECT mean(usage) as Mem "
disk_query1 = "SELECT mean(usage) as Disk "
query2 = "FROM RasPI "
cpu_query3 = "WHERE type = 'cpu' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
mem_query3 = "WHERE type = 'mem' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
disk_query3 = "WHERE type = 'disk' AND time > NOW() - {} AND time < NOW() - {} GROUP BY time({})".format(
self.q['range'],
self.q['end'],
self.q['granularity'])
query4 = "ORDER BY time DESC"
query_cpu = "{} {} {} {}".format(cpu_query1, query2, cpu_query3, query4)
query_mem = "{} {} {} {}".format(mem_query1, query2, mem_query3, query4)
query_disk = "{} {} {} {}".format(disk_query1, query2, disk_query3, query4)
measures_cpu = self.influx_status_client.query(query_cpu)
measures_mem = self.influx_status_client.query(query_mem)
measures_disk = self.influx_status_client.query(query_disk)
# Let's get the data from DB
header = "time,Cpu,Mem,Disk\n"
yield header
for cpu, mem, disk in zip(measures_cpu['RasPI'], measures_mem['RasPI'], measures_disk['RasPI']):
tm = str(cpu["time"]).strip()
try:
cpu_value = float(str(cpu["Cpu"]).strip())
except:
solar_value = ''
try:
mem_value = float(str(mem["Mem"]).strip())
except:
cap_value = ''
try:
disk_value = float(str(disk["Disk"]).strip())
except:
disk_value = ''
yield "{},{},{},{}\n".format(tm, cpu_value, mem_value, disk_value)
@cherrypy.expose
def historical_values(self, **kwargs):
'''
Function to all historical readings from InfluxDB.
These parsed into a CSV? Dict?
returns: csv in raw, text format
time, V_solar, I_solar
select mean(I_solar) as I_solar from voltage where time > now() - 10m group by time(30s) order by time desc
'''
# GET variables now set, ready to reference them
measure_range = kwargs['range']
query1 = "SELECT mean(V_array) as Uarr, mean(I_consumption) as Icon, "
query2 = "mean(V_solar) as Usol, mean(I_solar) as Isol, "
query3 = "mean(V_unit1) as Us1, mean(V_unit2) as Us2, max(charging) as charge FROM voltage"
query4 = "WHERE time > NOW() - {} GROUP BY time(30s)".format(measure_range)
query5 = "ORDER BY time DESC"
query = "{} {} {} {} {}".format(query1, query2, query3, query4, query5)
measures = self.influx_client.query(query)
# Let's get the data from DB
result = []
for datapoint in measures["voltage"]:
tm = datapoint['time']
try:
array_voltage = round(float(datapoint["Uarr"]), 2)
current_consumed = round(float(datapoint["Icon"]) / array_voltage, 2)
solar_voltage = round(float(datapoint["Usol"]), 2)
solar_current = round(float(datapoint["Isol"]) / solar_voltage , 2)
Us1 = round(float(datapoint["Us1"]), 2)
Us2 = round(float(datapoint["Us2"]), 2)
except:
continue
charging = int(datapoint["charge"])
row = {"time": tm,
"V_array": array_voltage,
"I_consumption": current_consumed,
"V_solar": solar_voltage,
"I_solar": solar_current,
"V_unit1": Us1,
"V_unit2": Us2,
"charging": charging}
result.append(row)
return result
@cherrypy.expose
def stat_values(self, **kwargs):
'''
Function to all historical readings from InfluxDB.
These parsed into a CSV? Dict?
returns: csv in raw, text format
24Wh_consumed, 24Wh_solar
select mean(I_solar) as I_solar from voltage where time > now() - 10m group by time(30s) order by time desc
'''
# GET variables now set, ready to reference them
_days = kwargs['days']
d = datetime.datetime.now()
measures = []
result = []
for t_range in range(1, _days*24, 24):
day = (d - datetime.timedelta(hours = t_range)).strftime("%Y-%m-%d")
query1 = "SELECT mean(V_array) as Uarr, mean(I_consumption) as Icon, "
query2 = "mean(V_solar) as Usol, mean(I_solar) as Isol, "
query3 = "mean(V_unit1) as Us1, mean(V_unit2) as Us2, max(charging) as charge FROM voltage"
query4 = "WHERE time > '{} 00:00:00' AND time < '{} 23:59:59' GROUP BY time(1h) fill(0)".format(day, day)
query5 = "ORDER BY time DESC"
query = "{} {} {} {} {}".format(query1, query2, query3, query4, query5)
measure = self.influx_client.query(query)
# Let's get the data from DB
tm = []
Ubat = []
Icon = []
Usol = []
Isol = []
Uss1 = []
Uss2 = []
P_cons = []
P_sol = []
charging = 0
day_result = []
for datapoint in measure["voltage"]:
#print(datapoint)
tstamp = datapoint['time']
array_voltage = round(float(datapoint["Uarr"]), 2)
current_consumed = round(float(datapoint["Icon"]), 2)
solar_voltage = round(float(datapoint["Usol"]), 2)
solar_current = round(float(datapoint["Isol"]), 2)
Us1 = round(float(datapoint["Us1"]), 2)
Us2 = round(float(datapoint["Us2"]), 2)
charge = int(datapoint["charge"])
p_consumed = round(float(array_voltage / 1000.00 \
* current_consumed / 1000.00), 2)
p_solar = round(float(solar_voltage / 1000.00 \
* solar_current / 1000.00), 2)
tm.append(tstamp),
Ubat.append(array_voltage),
Icon.append(current_consumed),
Usol.append(solar_voltage),
Isol.append(solar_current),
Uss1.append(Us1),
Uss2.append(Us2),
charging = charging + charge
P_cons.append(p_consumed)
P_sol.append(p_solar)
row = {"time": tm,
"V_array": Ubat,
"I_consumption": Icon,
"V_solar": Usol,
"I_solar": Isol,
"V_unit1": Uss1,
"V_unit2": Uss2,
"charging": charging,
"P_cons": P_cons,
"P_sol": P_sol}
try:
day_result = [
row['time'][0][0:10],
round(min(row['V_array']) / 1000, 2),
round(sum(row['I_consumption']) / 1000, 2),
round(max(row['V_solar']) / 1000, 2),
round(sum(row['I_solar']) / 1000, 2),
round(min(row['V_unit1']) / 1000, 2),
round(min(row['V_unit2']) / 1000, 2),
row['charging'],
round(sum(row['P_cons']),2),
round(sum(row['P_sol']), 2)
]
except:
day_result = [
d.strftime("%Y-%m-%d"),
0,
0,
0,
0,
0,
0,
0,
0,
0
]
# need to compute averages, mate...
print(day_result)
result.append(day_result)
return result
+1339
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+459
View File
@@ -0,0 +1,459 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */
html {
font-family: sans-serif;
line-height: 1.15;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
article,
aside,
footer,
header,
nav,
section {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
figcaption,
figure,
main {
display: block;
}
figure {
margin: 1em 40px;
}
hr {
-webkit-box-sizing: content-box;
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
-webkit-text-decoration-skip: objects;
}
a:active,
a:hover {
outline-width: 0;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: inherit;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
dfn {
font-style: italic;
}
mark {
background-color: #ff0;
color: #000;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
audio,
video {
display: inline-block;
}
audio:not([controls]) {
display: none;
height: 0;
}
img {
border-style: none;
}
svg:not(:root) {
overflow: hidden;
}
button,
input,
optgroup,
select,
textarea {
font-family: sans-serif;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html [type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
legend {
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
display: inline-block;
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details,
menu {
display: block;
}
summary {
display: list-item;
}
canvas {
display: inline-block;
}
template {
display: none;
}
[hidden] {
display: none;
}
html {
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
*,
*::before,
*::after {
-webkit-box-sizing: inherit;
box-sizing: inherit;
}
@-ms-viewport {
width: device-width;
}
html {
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 1rem;
font-weight: normal;
line-height: 1.5;
color: #292b2c;
background-color: #fff;
}
[tabindex="-1"]:focus {
outline: none !important;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: .5rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-original-title] {
cursor: help;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: bold;
}
dd {
margin-bottom: .5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
a {
color: #0275d8;
text-decoration: none;
}
a:focus, a:hover {
color: #014c8c;
text-decoration: underline;
}
a:not([href]):not([tabindex]) {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre {
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
}
figure {
margin: 0 0 1rem;
}
img {
vertical-align: middle;
}
[role="button"] {
cursor: pointer;
}
a,
area,
button,
[role="button"],
input,
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
}
table {
border-collapse: collapse;
background-color: transparent;
}
caption {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
color: #636c72;
text-align: left;
caption-side: bottom;
}
th {
text-align: left;
}
label {
display: inline-block;
margin-bottom: .5rem;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
input,
button,
select,
textarea {
line-height: inherit;
}
input[type="radio"]:disabled,
input[type="checkbox"]:disabled {
cursor: not-allowed;
}
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
display: block;
width: 100%;
padding: 0;
margin-bottom: .5rem;
font-size: 1.5rem;
line-height: inherit;
}
input[type="search"] {
-webkit-appearance: none;
}
output {
display: inline-block;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
/*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,::after,::before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}html{-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}body{font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#292b2c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{cursor:help}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse;background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#636c72;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,select,textarea{line-height:inherit}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}input[type=search]{-webkit-appearance:none}output{display:inline-block}[hidden]{display:none!important}/*# sourceMappingURL=bootstrap-reboot.min.css.map */
@@ -0,0 +1 @@
{"version":3,"sources":["../../scss/_normalize.scss","bootstrap-reboot.css","../../scss/_reboot.scss","../../scss/_variables.scss","../../scss/mixins/_hover.scss"],"names":[],"mappings":"4EAYA,KACE,YAAA,WACA,YAAA,KACA,qBAAA,KACA,yBAAA,KAUF,KACE,OAAA,EAOF,QAAA,MAAA,OAAA,OAAA,IAAA,QAME,QAAA,MAQF,GACE,UAAA,IACA,OAAA,MAAA,EAWF,WAAA,OAAA,KAGE,QAAA,MAOF,OACE,OAAA,IAAA,KAQF,GACE,mBAAA,YAAA,WAAA,YACA,OAAA,EACA,SAAA,QAQF,IACE,YAAA,UAAA,UACA,UAAA,IAWF,EACE,iBAAA,YACA,6BAAA,QAQF,SAAA,QAEE,cAAA,EAQF,YACE,cAAA,KACA,gBAAA,UACA,gBAAA,UAAA,OAOF,EAAA,OAEE,YAAA,QAOF,EAAA,OAEE,YAAA,OAQF,KAAA,IAAA,KAGE,YAAA,UAAA,UACA,UAAA,IAOF,IACE,WAAA,OAOF,KACE,iBAAA,KACA,MAAA,KAOF,MACE,UAAA,IAQF,IAAA,IAEE,UAAA,IACA,YAAA,EACA,SAAA,SACA,eAAA,SAGF,IACE,OAAA,OAGF,IACE,IAAA,MAUF,MAAA,MAEE,QAAA,aAOF,sBACE,QAAA,KACA,OAAA,EAOF,IACE,aAAA,KAOF,eACE,SAAA,OAWF,OAAA,MAAA,SAAA,OAAA,SAKE,YAAA,WACA,UAAA,KACA,YAAA,KACA,OAAA,EAQF,OAAA,MAEE,SAAA,QAQF,OAAA,OAEE,eAAA,KASF,aAAA,cAAA,OAAA,mBAIE,mBAAA,OAOF,gCAAA,+BAAA,gCAAA,yBAIE,aAAA,KACA,QAAA,EAOF,6BAAA,4BAAA,6BAAA,sBAIE,QAAA,IAAA,OAAA,WAOF,SACE,OAAA,IAAA,MAAA,OACA,OAAA,EAAA,IACA,QAAA,MAAA,OAAA,MAUF,OACE,mBAAA,WAAA,WAAA,WACA,MAAA,QACA,QAAA,MACA,UAAA,KACA,QAAA,EACA,YAAA,OAQF,SACE,QAAA,aACA,eAAA,SAOF,SACE,SAAA,KCrKF,gBAAA,aD+KE,mBAAA,WAAA,WAAA,WACA,QAAA,EC1KF,yCAAA,yCDmLE,OAAA,KC9KF,cDuLE,mBAAA,UACA,eAAA,KCnLF,4CAAA,yCD4LE,mBAAA,KAQF,6BACE,mBAAA,OACA,KAAA,QAWF,QAAA,KAEE,QAAA,MAOF,QACE,QAAA,UAUF,OACE,QAAA,aAOF,SACE,QAAA,KCnNF,SD8NE,QAAA,KEtbF,KACE,mBAAA,WAAA,WAAA,WAGF,EAAA,QAAA,SAGE,mBAAA,QAAA,WAAA,QAoBA,cAAgB,MAAA,aAQlB,KAYE,mBAAA,UAGA,4BAAA,YAGF,KACE,YAAA,cAAA,UAAA,mBAAA,WAAA,OC2K4H,iBD3K5H,MAAA,WACA,UAAA,KACA,YAAA,IACA,YAAA,IAEA,MAAA,QAEA,iBAAA,KD2LF,sBClLE,QAAA,YAYF,GAAI,GAAI,GAAI,GAAI,GAAI,GAClB,WAAA,EACA,cAAA,MAOF,EACE,WAAA,EACA,cAAA,KAIF,0BAAA,YAGE,OAAA,KAGF,QACE,cAAA,KACA,WAAA,OACA,YAAA,QAGF,GAAA,GAAA,GAGE,WAAA,EACA,cAAA,KAGF,MAAA,MAAA,MAAA,MAIE,cAAA,EAGF,GACE,YAAA,IAGF,GACE,cAAA,MACA,YAAA,EAGF,WACE,OAAA,EAAA,EAAA,KAQF,EACE,MAAA,QACA,gBAAA,KEhJE,QAAA,QFmJA,MAAA,QACA,gBAAA,UAUJ,8BACE,MAAA,QACA,gBAAA,KEhKE,oCAAA,oCFmKA,MAAA,QACA,gBAAA,KANJ,oCAUI,QAAA,EASJ,IAEE,WAAA,EAEA,cAAA,KAEA,SAAA,KAQF,OAGE,OAAA,EAAA,EAAA,KAQF,IAGE,eAAA,ODsIF,cCzHE,OAAA,QAcF,cAAA,EAAA,KAAA,OAAA,MAAA,MAAA,OAAA,QAAA,SASE,iBAAA,aAAA,aAAA,aAQF,MAEE,gBAAA,SAEA,iBAAA,YAGF,QACE,YAAA,OACA,eAAA,OACA,MAAA,QACA,WAAA,KACA,aAAA,OAGF,GAEE,WAAA,KAQF,MAEE,QAAA,aACA,cAAA,MAOF,aACE,QAAA,IAAA,OACA,QAAA,IAAA,KAAA,yBAGF,OAAA,MAAA,OAAA,SAME,YAAA,QAGF,8BAAA,2BAMI,OAAA,YAKJ,iBAAA,iBAAA,2BAAA,kBASE,mBAAA,QAGF,SAEE,OAAA,SAGF,SAME,UAAA,EAEA,QAAA,EACA,OAAA,EACA,OAAA,EAGF,OAEE,QAAA,MACA,MAAA,KACA,QAAA,EACA,cAAA,MACA,UAAA,OACA,YAAA,QAGF,mBAKE,mBAAA,KAIF,OACE,QAAA,aDsEF,SC9DE,QAAA"}
+9320
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+70
View File
@@ -0,0 +1,70 @@
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
/*
* Typography
*/
h1 {
margin-bottom: 20px;
padding-bottom: 9px;
border-bottom: 1px solid #eee;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
border-right: 1px solid #eee;
}
/* Sidebar navigation */
.sidebar {
padding-left: 0;
padding-right: 0;
}
.sidebar .nav {
margin-bottom: 20px;
}
.sidebar .nav-item {
width: 100%;
}
.sidebar .nav-item + .nav-item {
margin-left: 0;
}
.sidebar .nav-link {
border-radius: 0;
}
/*
* Dashboard
*/
/* Placeholders */
.placeholders {
padding-bottom: 3rem;
}
.placeholder img {
padding-top: 1.5rem;
padding-bottom: 1.5rem;
}
+75
View File
@@ -0,0 +1,75 @@
/*BODY{font-family:monospace; background-color: #fafafa; padding: 0px; margin:0px;}*/
div.textbox{
width: 50em;
margin-left: auto;
margin-right: auto;
margin-top: 2em;
padding: 3em;
border: 1px solid #ccc;
border-top: 1px solid #ddd;
border-bottom: 2px solid #000;
border-right: 2px solid #000;
background-color: #fff;
}
/*H1{font-size: 2em;}
H2{font-size: 1em; color: #666;}
h3{font-size: 0.8em; coloe: #eee;}
TABLE{border:1px solid grey;}
TD{padding: 2px;}
TH{background-color: #eee;}*/
IMG#profile{
float: left;
margin-right: 2em;
margin-bottom: 4em;
margin-top:4em;
}
#header{
border-bottom: 2px solid #000;
margin-bottom: 4em;
padding: 8em;
padding-top: 2em;
padding-bottom: 1em;
background-color: #fff;
}
#footer{
font-size: 0.5em;
color: #777;
margin: 0px;
margin-top: 4em;
padding: 10em;
padding-top: 2em;
padding-bottom: 4em;
border-top: 1px solid #ddd;
background-color: #fefefe;
}
#menu{
float: right;
}
.menuitem{
float: left;
font-size: 1.5em;
color: #000;
}
.menuitem_current{
float: left;
font-size: 1.5em;
color: #000;
font-weight: bold;
}
.menuitem a{
color: #666;
text-decoration: none;
}
.menuitem a:hover{
color: #600;
}
.cleaner{
clear: both;
display: inline;
height: 0px;
}
#logo{
}
.left{float: left;}
.right{float: right;}
+117
View File
@@ -0,0 +1,117 @@
/**
* Default styles for the dygraphs charting library.
*/
.dygraph-legend {
position: absolute;
font-size: 14px;
z-index: 10;
width: 250px; /* labelsDivWidth */
/*
dygraphs determines these based on the presence of chart labels.
It might make more sense to create a wrapper div around the chart proper.
top: 0px;
right: 2px;
*/
background: white;
line-height: normal;
text-align: left;
overflow: hidden;
}
/* styles for a solid line in the legend */
.dygraph-legend-line {
display: inline-block;
position: relative;
bottom: .5ex;
padding-left: 1em;
height: 1px;
border-bottom-width: 2px;
border-bottom-style: solid;
/* border-bottom-color is set based on the series color */
}
/* styles for a dashed line in the legend, e.g. when strokePattern is set */
.dygraph-legend-dash {
display: inline-block;
position: relative;
bottom: .5ex;
height: 1px;
border-bottom-width: 2px;
border-bottom-style: solid;
/* border-bottom-color is set based on the series color */
/* margin-right is set based on the stroke pattern */
/* padding-left is set based on the stroke pattern */
}
.dygraph-roller {
position: absolute;
z-index: 10;
}
/* This class is shared by all annotations, including those with icons */
.dygraph-annotation {
position: absolute;
z-index: 10;
overflow: hidden;
}
/* This class only applies to annotations without icons */
/* Old class name: .dygraphDefaultAnnotation */
.dygraph-default-annotation {
border: 1px solid black;
background-color: white;
text-align: center;
}
.dygraph-axis-label {
/* position: absolute; */
/* font-size: 14px; */
z-index: 10;
line-height: normal;
overflow: hidden;
color: black; /* replaces old axisLabelColor option */
}
.dygraph-axis-label-x {
}
.dygraph-axis-label-y {
}
.dygraph-axis-label-y2 {
}
.dygraph-title {
font-weight: bold;
z-index: 10;
text-align: center;
/* font-size: based on titleHeight option */
}
.dygraph-xlabel {
text-align: center;
/* font-size: based on xLabelHeight option */
}
/* For y-axis label */
.dygraph-label-rotate-left {
text-align: center;
/* See http://caniuse.com/#feat=transforms2d */
transform: rotate(90deg);
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-o-transform: rotate(90deg);
-ms-transform: rotate(90deg);
}
/* For y2-axis label */
.dygraph-label-rotate-right {
text-align: center;
/* See http://caniuse.com/#feat=transforms2d */
transform: rotate(-90deg);
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+52
View File
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="938px" height="938px" viewBox="0 0 938 938" style="enable-background:new 0 0 938 938;" xml:space="preserve">
<g>
<g>
<path d="M50.792,747.965c0,13.415,10.875,24.29,24.29,24.29h787.836c13.415,0,24.29-10.875,24.29-24.29v-408.36H50.792V747.965z
M649.792,476.871c0-6.419,5.203-11.622,11.622-11.622h112.098c6.419,0,11.622,5.203,11.622,11.622v27.54
c0,6.419-5.203,11.622-11.622,11.622H661.414c-6.419,0-11.622-5.203-11.622-11.622V476.871z M152.866,476.295
c0-6.091,4.938-11.029,11.029-11.029h36.764v-36.8c0-6.091,4.938-11.028,11.029-11.028h28.725c6.091,0,11.029,4.938,11.029,11.028
v36.8h36.764c6.091,0,11.029,4.938,11.029,11.029v28.727c0,6.091-4.938,11.028-11.029,11.028h-36.764v36.765
c0,6.091-4.938,11.028-11.029,11.028h-28.725c-6.091,0-11.029-4.938-11.029-11.028V516.05h-36.764
c-6.091,0-11.029-4.938-11.029-11.028V476.295L152.866,476.295z"/>
<path d="M915.285,314.605c12.545,0,22.715-10.17,22.715-22.715v-38.164c0-12.545-10.17-22.715-22.715-22.715H788.537v-42.552
c0-12.545-10.17-22.715-22.715-22.715h-97.83c-12.545,0-22.715,10.17-22.715,22.715v42.552H301.611v-42.552
c0-12.545-10.17-22.715-22.715-22.715h-97.83c-12.545,0-22.715,10.17-22.715,22.715v42.552H22.715
C10.17,231.01,0,241.181,0,253.727v38.165c0,12.545,10.17,22.715,22.715,22.715h28.076h836.417L915.285,314.605L915.285,314.605z"
/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+3535
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2017 The Bootstrap Authors
* Copyright 2014-2017 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
// See the Getting Started docs for more information:
// https://getbootstrap.com/getting-started/#support-ie10-width
(function () {
'use strict'
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement('style')
msViewportStyle.appendChild(
document.createTextNode(
'@-ms-viewport{width:auto!important}'
)
)
document.head.appendChild(msViewportStyle)
}
}())
+120
View File
@@ -0,0 +1,120 @@
var hours = 24;
var granularity = '2m';
var end = 0;
var graphdata = "https://bastart.spoton.cz/data/solar_monitor?range=24h&granularity=2m&end=0h";
sol = new Dygraph(
// containing div
document.getElementById("solar"),
// CSV or path to a CSV file.
graphdata
,{
//labels: ['time','V_solar','Isolar', P_solar, P_cons],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true
},
y2 : {
drawGrid: false,
drawAxis: true,
independentTicks: true
}
},
rollPeriod: 5,
visibility: [true, false, true, true],
interactionModel: {},
connectSeparatedPoints: true,
series:{
'V_solar': {
axis: 'y',
color: '#ffd020',
fillGraph: true,
fillAlpha: 0.4
},
'I_solar': {
axis: 'y',
color: '#ff1100'
},
'P_solar': {
axis: 'y2',
color: '#1111ff',
fillGraph: true,
fillAlpha: 0.4
},
'P_cons': {
axis: 'y2',
color: '#ff1111',
fillGraph: true,
fillAlpha: 0.4
}
},
ylabel: '<span style="color:#ffd020;">[V]</span>/<span style="color:#ff1100;">[A]</span>',
y2label: '<span style="color:#111177;">Solar / Consumption [W]</span>',
labelsDiv: 'solar_labels',
legend: 'always'
}
);
function refreshGraph(){
graphdata = "https://bastart.spoton.cz/data/solar_monitor?range=" + hours + "h&granularity=" + granularity + "&end=" + end + "h";
sol.updateOptions({'file': graphdata});
//power.updateOptions({'file': graphdata});
}
function setHours(hours_to_set){
hours = hours_to_set;
switch(hours){
case '1':
granularity = '30s';
break;
case '6':
granularity = '1m';
break;
case '12':
granularity = '1m';
break;
case '24':
granularity = '2m';
break;
case '168':
granularity = '20m';
break;
case '720':
granularity = '1h';
break;
default:
granularity = '10m';
}
end = 0;
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph();
}
function setBack(){
// range=1h -> range=2h&end=1h
disp_range = hours*1 - end*1;
hours = hours*1 + disp_range;
end = end*1 + disp_range;
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph();
}
function setForth(){
disp_range = hours*1 - end*1;
hours = hours*1 - disp_range;
if(hours < disp_range){
hours = disp_range;
}
end = end*1 - disp_range;
if(end < 0){
end = 0;
}
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph();
}
+174
View File
@@ -0,0 +1,174 @@
var hours = 24;
var granularity = '2m';
var end = 0;
var solcapdata = "https://bastart.spoton.cz/data/solcap_monitor?range=24h&granularity=2m&end=0h";
var cpumemdata = "https://bastart.spoton.cz/data/cpumem_monitor?range=24h&granularity=2m&end=0h";
solcap = new Dygraph(
// containing div
document.getElementById("solcap"),
// CSV or path to a CSV file.
solcapdata
,{
//labels: ['time','Solar','Capacitor'],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true,
valueRange: [0,4.6]
},
y2 : {
drawGrid: false,
drawAxis: true,
valueRange: [0,8.6],
independentTicks: true
}
},
rollPeriod: 5,
interactionModel: {},
connectSeparatedPoints: true,
series:{
'Solar': {
axis: 'y',
color: '#888844',
fillGraph: true,
fillAlpha: 0.4
},
'Capacitor': {
axis: 'y2',
color: '#884444'
}
},
ylabel: '<span style="color:#888844;">Solar Irradiance</span>',
y2label: '<span style="color:#884444;">Capacitor [V]</span>',
labelsDiv: 'solcap_labels',
legend: 'always'
}
);
cpumem = new Dygraph(
// containing div
document.getElementById("cpumem"),
// CSV or path to a CSV file.
cpumemdata
,{
//labels: ['time','cpu','mem','disk'],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true,
valueRange: [0,100]
},
y2 : {
drawGrid: false,
drawAxis: true,
valueRange: [0,100],
independentTicks: true
}
},
rollPeriod: 5,
interactionModel: {},
connectSeparatedPoints: true,
series:{
'Cpu': {
axis: 'y',
color: '#888844',
fillGraph: true,
fillAlpha: 0.4
},
'Mem': {
axis: 'y2',
color: '#884444'
},
'Disk': {
axis: 'y2',
color: '#448844'
}
},
ylabel: '<span style="color:#888844;">CPU [%]</span>',
y2label: '<span style="color:#884444;">MEM / DISK [%]</span>',
labelsDiv: 'cpumem_labels',
legend: 'always'
}
);
function refreshGraph(source){
graphdata = "https://bastart.spoton.cz/data/" + source + "?range=" + hours + "h&granularity=" + granularity + "&end=" + end + "h";
if(source == 'solcap_monitor'){
solcap.updateOptions({'file': graphdata});
}
if(source == 'cpumem_monitor'){
cpumem.updateOptions({'file': graphdata});
}
//document.getElementById('xxx').innerHTML = source;
//power.updateOptions({'file': graphdata});
}
function setHours(hours_to_set, target){
hours = hours_to_set;
switch(hours){
case '1':
granularity = '10s';
break;
case '6':
granularity = '2m';
break;
case '12':
granularity = '2m';
break;
case '24':
granularity = '5m';
break;
case '168':
granularity = '10m';
break;
case '720':
granularity = '1h';
break;
case '4320':
granularity = '6h';
break;
case '8640':
granularity = '1d';
break;
default:
granularity = '10m';
}
end = 0;
//document.getElementById('xxx').innerHTML = target;
refreshGraph(target);
}
function setBack(target){
// range=1h -> range=2h&end=1h
disp_range = hours*1 - end*1;
hours = hours*1 + disp_range;
end = end*1 + disp_range;
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph(target);
}
function setForth(target){
disp_range = hours*1 - end*1;
hours = hours*1 - disp_range;
if(hours < disp_range){
hours = disp_range;
}
end = end*1 - disp_range;
if(end < 0){
end = 0;
}
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph(target);
}
+296
View File
@@ -0,0 +1,296 @@
var hours = 24;
var granularity = '1m';
var end = 0;
var winddata = "https://bastart.spoton.cz/data/wind_monitor?range=24h&granularity=1m&end=0h";
var temphumidata = "https://bastart.spoton.cz/data/temphumi_monitor?range=24h&granularity=2m&end=0h";
var pressuredata = "https://bastart.spoton.cz/data/pressure_monitor?range=168h&granularity=2m&end=0h";
wind = new Dygraph(
// containing div
document.getElementById("wind"),
// CSV or path to a CSV file.
winddata
,{
//labels: ['time','Speed','Gust', 'Direction'],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true,
},
y2 : {
drawGrid: false,
drawAxis: true,
valueRange: [0,360],
independentTicks: true
}
},
rollPeriod: 20,
interactionModel: {},
connectSeparatedPoints: true,
series:{
'Speed': {
axis: 'y',
color: '#444444',
fillGraph: true,
fillAlpha: 0.4
},
'Gusts': {
axis: 'y',
color: '#ff5555'
},
'Direction': {
axis: 'y2',
color: '#999999'
}
},
ylabel: '<span style="color:#444444;">Speed<span style="color:#444444;"> / <span style="color:#ff5555;">Gusts</span> [km/h]',
y2label: '<span style="color:#999999;">Direction [&deg;]</span>',
labelsDiv: 'wind_labels',
legend: 'always'
}
);
temphumi_out = new Dygraph(
// containing div
document.getElementById("temphumi"),
// CSV or path to a CSV file.
temphumidata
,{
//labels: [time,T(ins),T(out),Humi(ins),Humi(out)],
axes : {
x : {
drawGrid: true,
drawAxis : false
},
y : {
drawGrid: false,
drawAxis : true,
},
y2 : {
drawGrid: false,
drawAxis: true,
independentTicks: true
}
},
rollPeriod: 10,
visibility: [false, true, false, true],
interactionModel: {},
connectSeparatedPoints: true,
series:{
'T(out)': {
axis: 'y',
color: '#705555',
fillGraph: true,
fillAlpha: 0.2
},
'T(ins)': {
axis: 'y',
color: '#705555',
fillGraph: true,
fillAlpha: 0.2
},
'Humi(out)': {
axis: 'y2',
color: '#222288'
},
'Humi(ins)': {
axis: 'y2',
color: '#222288'
}
},
ylabel: '<span style="color:#555555;">Outside [&deg;C]</span>',
y2label: '<span style="color:#222288;">Humidity [%]</span>',
labelsDiv: 'temphumi_labels',
legend: 'always'
}
);
temphumi_in = new Dygraph(
// containing div
document.getElementById("temphumi_in"),
// CSV or path to a CSV file.
temphumidata
,{
//labels: [time,T(ins),T(out),Humi(ins),Humi(out)],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true,
valueRange: [10,35]
},
y2 : {
drawGrid: false,
drawAxis: true,
independentTicks: true,
valueRange: [0,100]
}
},
rollPeriod: 10,
visibility: [true, false, true, false],
interactionModel: {},
connectSeparatedPoints: true,
series:{
'T(out)': {
axis: 'y',
color: '#705555',
fillGraph: true,
fillAlpha: 0.2
},
'T(ins)': {
axis: 'y',
color: '#705555',
fillGraph: true,
fillAlpha: 0.2
},
'Humi(out)': {
axis: 'y2',
color: '#222288'
},
'Humi(ins)': {
axis: 'y2',
color: '#222288'
}
},
ylabel: '<span style="color:#555555;">Inside [&deg;C]</span>',
y2label: '<span style="color:#222288;">Humidity [%]</span>',
labelsDiv: 'temphumi_labels_in',
legend: 'always'
}
);
pressure = new Dygraph(
// containing div
document.getElementById("pressure"),
// CSV or path to a CSV file.
pressuredata
,{
//labels: [time,Pressure],
axes : {
x : {
drawGrid: true,
drawAxis : true
},
y : {
drawGrid: false,
drawAxis : true,
valueRange: [970,1055]
},
y2 : {
drawGrid: false,
drawAxis: true,
valueRange: [970,1055]
}
},
rollPeriod: 10,
interactionModel: {},
connectSeparatedPoints: true,
visibility: [true, false],
series:{
'Pressure': {
axis: 'y',
color: '#557070',
fillGraph: true,
fillAlpha: 0.5
},
'Pressure': {
axis: 'y2',
color: '#557070',
fillGraph: true,
fillAlpha: 0.5
}
},
ylabel: '<span style="color:#555555;">Pressure [hPa]</span>',
labelsDiv: 'pressure_labels',
legend: 'always'
}
);
function refreshGraph(source){
graphdata = "https://bastart.spoton.cz/data/" + source + "?range=" + hours + "h&granularity=" + granularity + "&end=" + end + "h";
if(source == 'wind_monitor'){
wind.updateOptions({'file': graphdata});
}
if(source == 'temphumi_monitor'){
temphumi_out.updateOptions({'file': graphdata});
temphumi_in.updateOptions({'file': graphdata});
}
if(source == 'pressure_monitor'){
pressure.updateOptions({'file': graphdata});
}
//document.getElementById('xxx').innerHTML = source;
//power.updateOptions({'file': graphdata});
}
function setHours(hours_to_set, target){
hours = hours_to_set;
switch(hours){
case '1':
granularity = '1m';
if(target == 'temphumi_monitor'){ granularity = '2m';}
break;
case '6':
granularity = '1m';
if(target == 'temphumi_monitor'){ granularity = '2m';}
break;
case '12':
granularity = '1m';
if(target == 'temphumi_monitor'){ granularity = '2m';}
break;
case '24':
granularity = '1m';
if(target == 'temphumi_monitor'){ granularity = '2m';}
break;
case '168':
granularity = '10m';
break;
case '720':
granularity = '1h';
break;
case '4320':
granularity = '6h';
break;
case '8640':
granularity = '1d';
break;
default:
granularity = '10m';
}
end = 0;
//document.getElementById('xxx').innerHTML = target;
refreshGraph(target);
}
function setBack(target){
// range=1h -> range=2h&end=1h
disp_range = hours*1 - end*1;
hours = hours*1 + disp_range;
end = end*1 + disp_range;
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph(target);
}
function setForth(target){
disp_range = hours*1 - end*1;
hours = hours*1 - disp_range;
if(hours < disp_range){
hours = disp_range;
}
end = end*1 - disp_range;
if(end < 0){
end = 0;
}
//document.getElementById('xxx').innerHTML = graphdata;
refreshGraph(target);
}
Executable
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/python3
import os
import sys
import cherrypy
import influxdb
import time
import json
# Universal variables
_SCRIPT_PATH = os.path.dirname(sys.argv[0])
_STATIC_DIR = '/templates/' # Needs to have trailing and leading slash '/'
influx_host = 'localhost'
influx_port = 8086
influx_user = 'pi'
influx_pwd = 'freedavis'
influx_status_db = 'status'
# Icons
fs_sol_icon = '../static/img/iss_solar_icon.png'
fs_cap_icon = '../static/img/iss_capacitor_icon.png'
# Functions
def read_html(filename):
read_path = _SCRIPT_PATH + _STATIC_DIR + filename + '.html'
try:
with open(read_path, 'r') as handle:
return handle.read()
except:
return """<div>ERROR!</div>""" + read_path
# Set default structure
html_start = '<html>'
body_start = '<body>'
body_close = '</body>'
html_close = '</html>'
class StatusInfo(object):
@cherrypy.expose
def index(self):
header = read_html('header')
menu_raw = read_html('top_menu')
menu = menu_raw.format(energy = '', weather = '', status = 'active')
body = self.body()
footer = read_html('footer')
result = header\
+ menu\
+ body\
+ footer
return result
def LastKnownState(self):
'''
Function to get energy readings from InfluxDB.
returns:
dict(): {"time": str(time_stamp),
"solar": solar,
"cap": cap}
'''
influx_status_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_status_db
)
# General query
query1 = "SELECT time, voltage FROM iss WHERE type = "
query2 = "ORDER BY time DESC LIMIT 1"
# now parse the tag in the middle of the two q from above
solar_q = "{} '{}' {}".format(query1, "solar", query2)
cap_q = "{} '{}' {}".format(query1, "capcaitor", query2)
# the actual query to DB
solar = influx_status_client.query(solar_q)
cap = influx_status_client.query(cap_q)
# returned is a list, in this case, we just need one value [0]
result_solar = [sol for sol in solar][0][0]
result_cap = [cap_ for cap_ in cap][0][0]
# Put the time to a uhman readable format, strip nanosecs
time_stamp = time.strptime(result_solar['time'].split('.')[0],
"%Y-%m-%dT%H:%M:%S")
result = {}
# Construct the result to return
result.update({"time": time_stamp})
result.update({"solar": round(result_solar['voltage'], 1)})
result.update({"cap": round(result_cap['voltage'], 1)})
return result
def body(self):
admin_preformat = read_html('status_admin')
current_status = self.LastKnownState()
admin_html = admin_preformat.format(
timestamp = time.strftime("%d.%m.%Y %H:%M:%S",
current_status['time']),
sol_icon = fs_sol_icon,
cap_icon = fs_cap_icon,
sol_value = round(current_status['solar'] / 100, 2),
cap_value = current_status['cap']
)
return admin_html
+7
View File
@@ -0,0 +1,7 @@
<div id="footer">
Created in 101 minutes, in VI, while sitting in a train Wien -> Bratislava.
Uploeaded over EDGE with a lot of signal-less territory on roaming. Cheers.
Done by Ventil.
</div>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Energy reporter</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="Content-Language" content="en">
<meta name="Robots" content="all">
<meta name="Keywords" content="automation, python, development, arduino, measurement">
<meta name="description" content="RasPi automation hardcore">
<meta name="Author" content="Spoton s.r.o. | Milan Toman">
<link rel="stylesheet" type="text/css" href="./static/css/def_style.css">
<!-- Bootstrap core CSS -->
<link href="../static/css/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="../static/css/dashboard.css" rel="stylesheet">
<!-- Dygraph css and js -->
<link href="../static/css/dygraph.css" rel="stylesheet">
<script type="text/javascript" src="../static/js/dygraph.js"></script>
<link rel="icon" href="../../favicon.ico">
</head>
<body>
+65
View File
@@ -0,0 +1,65 @@
<div class="container-fluid">
<div class="row">
<div class="col"></div>
<main class="pt-4 col-10">
<h1>Status Dashboard</h1>
<h5>{timestamp} UTC</h5>
<section class="row text-center placeholders">
<div class="col placeholder">
<img src="{sol_icon}" width="20" class="img-fluid" alt="ISS solar irradiation">
<h3>{sol_value}</h3>
<div class="text-muted">[V]</div>
</div>
<div class="col placeholder">
<img src="{cap_icon}" width="20" class="img-fluid" alt="ISS Capacitor state">
<h3>{cap_value}</h3>
<div class="text-muted">[V]</div>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<div id="solcap_labels"></div>
<div id="solcap" class="container-fluid" style="width:100%;height:200px;"></div>
<button type="button" class="btn btn-secondary" onclick="setBack('solcap_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','solcap_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','solcap_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','solcap_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','solcap_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720','solcap_monitor')">Month</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('4320','solcap_monitor')">6Mo</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('8640','solcap_monitor')">Year</button>
<button type="button" class="btn btn-secondary" onclick="setForth('solcap_monitor')">&gt;&gt;</button>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<div id="cpumem_labels"></div>
<div id="cpumem" class="container-fluid" style="width:100%;height:200px;"></div>
<button type="button" class="btn btn-secondary" onclick="setBack('cpumem_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('1','cpumem_monitor')">1h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','cpumem_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','cpumem_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','cpumem_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','cpumem_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720','cpumem_monitor')">Month</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('4320','cpumem_monitor')">6Mo</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('8640','cpumem_monitor')">Year</button>
<button type="button" class="btn btn-secondary" onclick="setForth('cpumem_monitor')">&gt;&gt;</button>
</div>
<script type="text/javascript" src="../static/js/solcap_graph.js"></script>
</section>
</main>
<div class="col"></div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="../static/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../static/js/ie10bugfix.js"></script>
+20
View File
@@ -0,0 +1,20 @@
<nav class="navbar navbar-toggleable-md navbar-inverse fixed-top bg-inverse">
<button class="navbar-toggler navbar-toggler-right hidden-lg-up" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="#">Monitoring</a>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item {energy}">
<a class="nav-link" href="https://bastart.spoton.cz">Energy</a>
</li>
<li class="nav-item {weather}">
<a class="nav-link" href="https://bastart.spoton.cz/weather">Weather</a>
</li>
<li class="nav-item {status}">
<a class="nav-link" href="https://bastart.spoton.cz/status">Status</a>
</li>
</ul>
</div>
</nav>
+74
View File
@@ -0,0 +1,74 @@
<div class="container-fluid">
<div class="row">
<div class="col"></div>
<main class="pt-4 col-10">
<h1>Energy Dashboard</h1>
<section class="row text-center placeholders">
<div class="col placeholder">
<img src="{array_voltage_icon}" width="20" class="img-fluid" alt="Battery voltage">
<h3>{array_voltage} [V]</h3>
<div class="text-muted">{array_percent} [%]</div>
</div>
<div class="col placeholder">
<img src="../static/img/battery_current.png" width="20" class="img-fluid" alt="Battery current">
<h3>{charge_current}</h3>
<div class="text-muted">Charging [A]</div>
</div>
<div class="col placeholder">
<img src="../static/img/solar_power.png" width="20" class="img-fluid" alt="Solar array power">
<h3>{solar_power}</h3>
<span class="text-muted"> Solar PV [W]</span>
</div>
<div class="col placeholder">
<img src="../static/img/solar_voltage.png" width="20" class="img-fluid" alt="Solar array voltage">
<h3>{voltage_solar}</h3>
<div class="text-muted">Solar [V]</div>
</div>
<div class="col placeholder">
<img src="../static/img/pmax_day.png" width="20" class="img-fluid" alt="Max power / day">
<h3>{pmax_day}</h3>
<div class="text-muted">Pmax / day [W]</div>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<div id="solar_labels"></div>
<div id="solar" class="container-fluid" style="width:100%;"></div>
<div id="power_labels"></div>
<div id="power" class="container-fluid" style="width:100%;"></div>
<script type="text/javascript" src="../static/js/solar_graph.js"></script>
<button type="button" class="btn btn-secondary" onclick="setBack()">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720')">Month</button>
<button type="button" class="btn btn-secondary" onclick="setForth()">&gt;&gt;</button>
</div>
<div id="xxx"></div>
</section>
<div class="table-responisve">
<h2>Historical values by day</h2>
{history_table}
</div>
<div class="table-responsive">
{stats_table}
{fresh_table}
</div>
</main>
<div class="col"></div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="../static/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../static/js/ie10bugfix.js"></script>
+108
View File
@@ -0,0 +1,108 @@
<div class="container-fluid">
<div class="row">
<div class="col"></div>
<main class="pt-4 col-10">
<h1>Weather Dashboard</h1>
<h5>{timestamp} UTC</h5>
<section class="row text-center placeholders">
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_speed_icon}" width="50" class="img-fluid" alt="Wind Speed">
<h4>{w_speed_km} [km/h]</h4>
<div class="text-muted">{w_speed_ms} [m/s]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_gust_icon}" width="50" class="img-fluid" alt="Wind Gusts">
<h4>{w_gust_km} [km/h]</h4>
<div class="text-muted">{w_gust_ms} [m/s]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_dir_icon}" width="50" class="img-fluid" alt="Wind Direction">
<h4>{w_dir_name}</h4>
<div class="text-muted">{w_dir_deg} &deg;</div>
</div>
</section>
<section class="row text-center placeholders">
<div class="col-md-4 placeholder col-sm-6">
<img src="{out_temp_icon}" width="50" class="img-fluid" alt="Outside Temperature">
<h4>{out_temp} &deg;C</h4>
<div class="text-muted">[Outside]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{in_temp_icon}" width="50" class="img-fluid" alt="Inside Temperature">
<h4>{in_temp} &deg;C</h4>
<div class="text-muted">[Inside]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{pressure_icon}" width="50" class="img-fluid" alt="Atmospheric pressure">
<h4>{pressure} [hPa]</h4>
<div class="text-muted">{raw_pressure} [hPa] raw</div>
</div>
</section>
<section class="row text-center">
<div class="col">
<h2>Wind speed and direction</h2>
<div id="wind_labels"></div>
<div id="wind" class="container-fluid" style="width:100%;height:200px;"></div>
<div class="row"><div class="col py-5">
<button type="button" class="btn btn-secondary" onclick="setBack('wind_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','wind_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','wind_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','wind_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','wind_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720','wind_monitor')">Month</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('4320','wind_monitor')">6Mo</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('8640','wind_monitor')">Year</button>
<button type="button" class="btn btn-secondary" onclick="setForth('wind_monitor')">&gt;&gt;</button>
</div></div>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<h2>Temperature and Humidity</h2>
<div id="temphumi_labels"></div><div id="temphumi_labels_in"></div>
<div id="temphumi" class="container-fluid" style="width:100%;height:200px;"></div>
<div id="temphumi_in" class="container-fluid" style="width:100%;height:200px;"></div>
<button type="button" class="btn btn-secondary" onclick="setBack('temphumi_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','temphumi_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','temphumi_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','temphumi_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','temphumi_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720','temphumi_monitor')">Month</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('4320','temphumi_monitor')">6Mo</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('8640','temphumi_monitor')">Year</button>
<button type="button" class="btn btn-secondary" onclick="setForth('temphumi_monitor')">&gt;&gt;</button>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<div id="pressure_labels"></div>
<div id="pressure" class="container-fluid" style="width:100%;height:200px;"></div>
<button type="button" class="btn btn-secondary" onclick="setBack('pressure_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','pressure_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','pressure_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','pressure_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('720','pressure_monitor')">Month</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('4320','pressure_monitor')">6Mo</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('8640','pressure_monitor')">Year</button>
<button type="button" class="btn btn-secondary" onclick="setForth('pressure_monitor')">&gt;&gt;</button>
</div>
<script type="text/javascript" src="../static/js/weather_graph.js"></script>
</section>
</main>
<div class="col" id="xxx"></div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="../static/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../static/js/ie10bugfix.js"></script>
+280
View File
@@ -0,0 +1,280 @@
<div class="container-fluid">
<div class="row">
<!--
--------------------------- Nahodou menu na levo -----------------------
--
<nav class="col-sm-3 col-md-2 hidden-xs-down bg-faded sidebar">
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link active" href="#">Overview <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Reports</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Analytics</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Export</a>
</li>
</ul>
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link" href="#">Nav item</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Nav item again</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">One more nav</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another nav item</a>
</li>
</ul>
<ul class="nav nav-pills flex-column">
<li class="nav-item">
<a class="nav-link" href="#">Nav item again</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">One more nav</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Another nav item</a>
</li>
</ul>
</nav>
-->
<div class="col"></div>
<main class="pt-4 col-10">
<h1>Weather Dashboard</h1>
<h5>{timestamp}</h5>
<section class="row text-center placeholders">
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_speed_icon}" width="50" class="img-fluid" alt="Wind Speed">
<h4>{w_speed_km} [km/h]</h4>
<div class="text-muted">{w_speed_ms} [m/s]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_gust_icon}" width="50" class="img-fluid" alt="Wind Gusts">
<h4>{w_gust_km} [km/h]</h4>
<div class="text-muted">{w_gust_ms} [m/s]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{w_dir_icon}" width="50" class="img-fluid" alt="Wind Direction">
<h4>{w_dir_name}</h4>
<div class="text-muted">{w_dir_deg} &deg;</div>
</section>
<section class="row text-center placeholders">
<div class="col-md-4 placeholder col-sm-6">
<img src="{out_temp_icon}" width="50" class="img-fluid" alt="Outside Temperature">
<h4>{out_temp} &deg;C</h4>
<div class="text-muted">[Outside]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{in_temp_icon}" width="50" class="img-fluid" alt="Inside Temperature">
<h4>{in_temp} &deg;C</h4>
<div class="text-muted">[Inside]</div>
</div>
<div class="col-md-4 placeholder col-sm-6">
<img src="{pressure_icon}" width="50" class="img-fluid" alt="Atmospheric pressure">
<h4>{pressure} [hPa] Adjusted</h4>
<div class="text-muted">{raw_pressure} [hPa] Actual</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<h2>Wind speed and direction</h2>
<div id="wind_labels"></div>
<div id="wind" class="container-fluid" style="width:100%;height:200px;"></div>
<div class="row"><div class="col py-5">
<button type="button" class="btn btn-secondary" onclick="setBack('wind_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('1','wind_monitor')">1h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','wind_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','wind_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','wind_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','wind_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('5040','wind_monitor')">Month</button>
<button type="button" class="btn btn-secondary" onclick="setForth('wind_monitor')">&gt;&gt;</button>
</div></div>
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<h2>Temperature and Humidity</h2>
<div id="temphumi_labels"></div><div id="temphumi_labels_in"></div>
<div id="temphumi" class="container-fluid" style="width:100%;height:200px;"></div>
<div id="temphumi_in" class="container-fluid" style="width:100%;height:200px;"></div>
<button type="button" class="btn btn-secondary" onclick="setBack('temphumi_monitor')">&lt;&lt;</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('1','temphumi_monitor')">1h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('6','temphumi_monitor')">6h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('12','temphumi_monitor')">12h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('24','temphumi_monitor')">24h</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('168','temphumi_monitor')">Week</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="setHours('5040','temphumi_monitor')">Month</button>
<<<<<<< HEAD
<button type="button" class="btn btn-secondary" onclick="setForth('temphumi_monitor');">&gt;&gt;</button>
=======
<button type="button" class="btn btn-secondary" onclick="setForth('temphumi_monitor')">&gt;&gt;</button>
>>>>>>> ca62f04757f1010262e5559bbd8753bfa49bf34e
</div>
</section>
<section class="row text-center placeholders">
<div class="col">
<div id="pressure_labels"></div>
<div id="pressure" class="container-fluid" style="width:100%;"></div>
</div>
<script type="text/javascript" src="../static/js/weather_graph.js"></script>
</section>
</main>
<div class="col" id="xxx"></div>
<!--
<h2>Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
<tr>
<td>1,003</td>
<td>libero</td>
<td>Sed</td>
<td>cursus</td>
<td>ante</td>
</tr>
<tr>
<td>1,004</td>
<td>dapibus</td>
<td>diam</td>
<td>Sed</td>
<td>nisi</td>
</tr>
<tr>
<td>1,005</td>
<td>Nulla</td>
<td>quis</td>
<td>sem</td>
<td>at</td>
</tr>
<tr>
<td>1,006</td>
<td>nibh</td>
<td>elementum</td>
<td>imperdiet</td>
<td>Duis</td>
</tr>
<tr>
<td>1,007</td>
<td>sagittis</td>
<td>ipsum</td>
<td>Praesent</td>
<td>mauris</td>
</tr>
<tr>
<td>1,008</td>
<td>Fusce</td>
<td>nec</td>
<td>tellus</td>
<td>sed</td>
</tr>
<tr>
<td>1,009</td>
<td>augue</td>
<td>semper</td>
<td>porta</td>
<td>Mauris</td>
</tr>
<tr>
<td>1,010</td>
<td>massa</td>
<td>Vestibulum</td>
<td>lacinia</td>
<td>arcu</td>
</tr>
<tr>
<td>1,011</td>
<td>eget</td>
<td>nulla</td>
<td>Class</td>
<td>aptent</td>
</tr>
<tr>
<td>1,012</td>
<td>taciti</td>
<td>sociosqu</td>
<td>ad</td>
<td>litora</td>
</tr>
<tr>
<td>1,013</td>
<td>torquent</td>
<td>per</td>
<td>conubia</td>
<td>nostra</td>
</tr>
<tr>
<td>1,014</td>
<td>per</td>
<td>inceptos</td>
<td>himenaeos</td>
<td>Curabitur</td>
</tr>
<tr>
<td>1,015</td>
<td>sodales</td>
<td>ligula</td>
<td>in</td>
<td>libero</td>
</tr>
</tbody>
</table>
</div>
-->
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script>window.jQuery || document.write('<script src="../../assets/js/vendor/jquery.min.js"><\/script>')</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="../static/js/bootstrap.min.js"></script>
<!-- IE10 viewport hack for Surface/desktop Windows 8 bug -->
<script src="../static/js/ie10bugfix.js"></script>
Executable
+317
View File
@@ -0,0 +1,317 @@
#!/usr/bin/python3
import os
import sys
import cherrypy
import influxdb
import time
import datetime
import json
from dynamic import DynamicData
# Universal variables
_SCRIPT_PATH = os.path.dirname(sys.argv[0])
_STATIC_DIR = '/templates/' # Needs to have trailing and leading slash '/'
influx_host = 'localhost'
influx_port = 8086
influx_user = 'pi'
influx_pwd = 'freedavis'
influx_db = 'voltage'
_MEASURES_IN_MINUTE = 11
# Functions
def read_html(filename):
read_path = _SCRIPT_PATH + _STATIC_DIR + filename + '.html'
try:
with open(read_path, 'r') as handle:
return handle.read()
except:
return """<div>ERROR!</div>""" + read_path
# Set default structure
html_start = '<html>'
body_start = '<body>'
body_close = '</body>'
html_close = '</html>'
class EnergyInfo(object):
def __init__(self):
self.influx_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_db
)
self.array_full = '../static/img/battery_full.png' #100-90
self.array_80 = '../static/img/battery_80.png' # 90-70
self.array_60 = '../static/img/battery_60.png' # 70-50
self.array_40 = '../static/img/battery_40.png' # 50-30
self.array_20 = '../static/img/battery_20.png' # 30-10
self.array_0 = '../static/img/battery_0.png' # 10-0
self.array_ch = '../static/img/battery_charging.png'
self.battery_icon = self.array_full
self.measures_obj = DynamicData()
def set_battery_icon(self, percentage, state):
''' Interprets % of battery state in icon changes
expects:
float(percentage): Percentage -> battery icon
sets:
str(self.battery_icon): the path to appropriate icon image
'''
if state == 1:
self.battery_icon = self.array_ch
else:
if percentage > 70.0 and percentage < 89.9:
self.battery_icon = self.array_80
if percentage > 50.0 and percentage < 69.9:
self.battery_icon = self.array_60
if percentage > 30.0 and percentage < 49.9:
self.battery_icon = self.array_40
if percentage > 10.0 and percentage < 29.9:
self.battery_icon = self.array_20
if percentage > 0.0 and percentage < 9.9:
self.battery_icon = self.array_0
def percentageCalc(self, voltage, system):
''' Turns current charge for lead acid batteries into a human readable %
expects:
float(voltage): Voltage in V
int(system): nominal system voltage, e.g. 12, 24, 48 etc
returns:
float(percentage): Two decimal state of battery in percentage
'''
if system is 12:
percentage = round(24.5724168782\
* voltage * voltage - 521.9890329784 * voltage\
+ 2771.1828105637, 1)
elif system is 24:
percentage = 2.4442 * voltage * voltage\
- 82.004 * voltage + 602.91
percentage = 100.00 if percentage > 100.00 else percentage
return percentage
@cherrypy.expose
def index(self):
header = read_html('header')
menu_raw = read_html('top_menu')
menu = menu_raw.format(energy = 'active', weather = '', status = '')
body = self.body()
footer = read_html('footer')
result = header\
+ menu\
+ body\
+ footer
return result
def FreshValues(self):
''' Get most up-to-date energy reading.
returns:
dict(): {'I_solar': float(mA),
'V_unit2': float(mV),
'time': 'YYYY-mm-DDTHH:MM:SS.149636706Z',
'I_array': None, # ((deprecated, will be removed))
'V_unit1': float(mV),
'V_solar': float(mV),
'I_consumption': float(mA),
'V_array': float(mV),
'charging': bool(0/1)}
'''
query = "SELECT * FROM voltage ORDER BY time DESC LIMIT 1"
last_measure = self.influx_client.query(query)
charge_current = self.influx_client.query("select current from mppt where type = 'bat' order by time desc limit 1")
pmax_day = self.influx_client.query("select Pmax from mppt where type = 'day' order by time desc limit 1")
wh_day = self.influx_client.query("select Wh from mppt where type = 'day' order by time desc limit 1")
power_solar = self.influx_client.query("select power from mppt where type = 'solar' order by time desc limit 1")
state_r = self.influx_client.query("select * from mppt order by time desc limit 1")
result = [measure for measure in last_measure["voltage"]][0]
charge_current_result = [ch_curr for ch_curr in charge_current["mppt"]][0]["current"]
pmax_day_result = [pmax for pmax in pmax_day["mppt"]][0]["Pmax"]
wh_day_result = [wh for wh in wh_day["mppt"]][0]["Wh"]
power_solar_result = [Psol for Psol in power_solar["mppt"]][0]["power"]
state_result = [state for state in state_r["mppt"]][0]["state"]
time_stamp = time.strptime(result['time'].split('.')[0],
"%Y-%m-%dT%H:%M:%S")
result['time'] = time_stamp
result['state'] = state_result
result['Psol'] = power_solar_result
result['Wh_day'] = wh_day_result
result['Pmax_day'] = pmax_day_result
result['ChCurr'] = charge_current_result
return result
def HistoryValues(self, _INTERVAL):
''' Function to get energy readings from InfluxDB.
returns:
dict(): {"voltage_data": [{time: str(), V_array: str()...}, {...}],
"voltage_data_cli": [str(line1), str(line2)...]}
'''
table_list = []
query1 = "SELECT * FROM voltage"
query2 = "ORDER BY time DESC".format(_INTERVAL)
query = "{} {}".format(query1, query2)
neasured_range = {'range': '1h'}
measures = self.measures_obj.historical_values(range = '1h')
# Let's get the data from DB
for datapoint in measures:
time_stamp = time.strptime(datapoint["time"].split('.')[0],
"%Y-%m-%dT%H:%M:%SZ")
array_volts = round(datapoint["V_array"] / 1000, 2)
current_consumption = round(datapoint["I_consumption"] / 1000, 2)
solar_volts = round(datapoint["V_solar"] / 1000, 2)
current_solar = round(datapoint["I_solar"] / 1000, 2)
unit1_volts = round(datapoint["V_unit1"] / 1000, 2)
unit2_volts = round(datapoint["V_unit2"] / 1000, 2)
charging = datapoint["charging"]
power_consumption = round(current_consumption * 48, 2)
power_solar = round(current_solar * solar_volts, 2)
percent_array = self.percentageCalc(array_volts, 24)
percent_unit1 = self.percentageCalc(unit1_volts, 12)
percent_unit2 = self.percentageCalc(unit2_volts, 12)
table_list.append([time.strftime("%Y/%m/%d %H:%M:%S", time_stamp),
array_volts,
current_consumption,
solar_volts,
current_solar,
unit1_volts,
unit2_volts,
charging,
power_consumption,
power_solar])
print(table_list)
return {"voltage_data": table_list}
def tableConstructor(self, header, body):
''' The idea behind is to have a method of constructing <table> html
element and to separate data and html code.
expects:
list(header): [[str(header1), str(header2), ...], row2, row3, ...]
list(body): [str(td1), str(td2), ...]
returns:
str(html_table)
'''
table_header = ''
table_body = ''
table_begin = '<table class="table table-striped">'
thead = '<thead>'
thead_end = '</thead>'
tbody = '<tbody>'
tbody_end = '</tbody>'
table_end = '</table>'
table_header = '</tr>'
for th in header:
table_header = table_header + '<th>' + th + '</th>'
table_header = table_header + '</tr>'
for tr in body:
table_body = table_body + '<tr>'
for td in tr:
table_body = table_body + '<td>' + str(td) + '</td>'
table_body = table_body + '</tr>'
html_table = "{} {} {} {} {}".format(table_begin + thead,
table_header,
thead_end + tbody,
table_body,
tbody_end + table_end)
return html_table
def body(self):
lastState = self.FreshValues()
fresh_time = time.strftime("%Y-%m-%d %H:%M:%S", lastState['time'])
try:
fresh_voltage_array = round(float(lastState['V_array'] / 1000), 2)
except:
fresh_voltage_array = -99
fresh_ch_current = lastState['ChCurr']
try:
fresh_voltage_solar = round(float(lastState['V_solar'] / 1000), 2)
except:
fresh_voltage_solar = -99
fresh_current_solar = 10 #round(float(lastState['I_solar'] / 1000), 2)
try:
fresh_pmax_day = lastState['Pmax_day']
except:
fresh_pmax_day = -99
fresh_voltage_unit2 = 10 #round(float(lastState['V_unit2'] / 1000), 2)
fresh_percent_array = self.percentageCalc(fresh_voltage_array, 24)
fresh_percent_unit2 = self.percentageCalc(fresh_voltage_unit2, 12)
fresh_power_consumption = round(48\
* fresh_ch_current, 2)
fresh_power_solar = round(lastState['Psol'])
fresh_charge_state = lastState['charging']
fresh_table_header = ['Date Time',
'Uarr [V]',
'Icons [A]',
'Usol [V]',
'Isol [A]',
'Us1 [V]',
'Us2 [V]',
'Charge',
'Pcons [W]',
'Psol [W]']
fresh_table_contents = [[fresh_time,
fresh_voltage_array,
fresh_ch_current,
fresh_voltage_solar,
fresh_current_solar,
fresh_voltage_unit2,
fresh_charge_state,
fresh_power_consumption,
fresh_power_solar]]
fresh_table = self.tableConstructor(fresh_table_header,
fresh_table_contents)
fresh_table = ''
#history_table_header = fresh_table_header
#history_table_contents = self.HistoryValues(10)["voltage_data"]
#history_table = self.tableConstructor(history_table_header,
# history_table_contents)
history_table = ''
# Statistical table preparation
stat_header = ['Date',
'Uarr min [V]',
'Icons [Ah]',
'Usol max [V]',
'Isol [Ah]',
'Us1 min [V]',
'Us2 min [V]',
'Charge [h]',
'Pcons [Wh]',
'Psol [Wh]']
stats_content = self.measures_obj.stat_values(days = 7)
stats_table = self.tableConstructor(stat_header, stats_content)
#self.set_battery_icon(array_percent_adjusted, fresh_charge_state)
admin_preformat = read_html('voltage_admin')
admin_html = admin_preformat.format(array_voltage = fresh_voltage_array,
charge_current = fresh_ch_current,
solar_power = fresh_power_solar,
voltage_solar = fresh_voltage_solar,
pmax_day = fresh_pmax_day,
array_voltage_icon = '../static/img/battery_full.png',
array_percent = 0,
unit1_percentage = 0,
unit2_percentage = 0,
fresh_table = fresh_table,
history_table = history_table,
stats_table = stats_table)
return admin_html
Executable
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/python3
import os
import sys
import cherrypy
import influxdb
import time
import json
# Universal variables
_SCRIPT_PATH = os.path.dirname(sys.argv[0])
_STATIC_DIR = '/templates/' # Needs to have trailing and leading slash '/'
influx_host = 'localhost'
influx_port = 8086
influx_user = 'pi'
influx_pwd = 'freedavis'
influx_weather_db = 'weather'
influx_status_db = 'status'
# Icons
fs_wind_icon = '../static/img/wind_icon.png'
fs_pressure_icon = '../static/img/pressure_icon.png'
fs_in_temperature_icon = '../static/img/inside_temp_icon.png'
fs_out_temperature_icon = '../static/img/outside_temp_icon.png'
fs_windgust_icon = '../static/img/wind_gust_icon.png'
fs_winddirection_icon = '../static/img/wind_direction_icon.png'
# Functions
def read_html(filename):
read_path = _SCRIPT_PATH + _STATIC_DIR + filename + '.html'
try:
with open(read_path, 'r') as handle:
return handle.read()
except:
return """<div>ERROR!</div>""" + read_path
# Set default structure
html_start = '<html>'
body_start = '<body>'
body_close = '</body>'
html_close = '</html>'
class WeatherInfo(object):
@cherrypy.expose
def index(self):
header = read_html('header')
menu_raw = read_html('top_menu')
menu = menu_raw.format(energy = '', weather = 'active', status = '')
body = self.body()
footer = read_html('footer')
result = header\
+ menu\
+ body\
+ footer
return result
def winddirName(self, direction):
'''
Function to get energy readings from InfluxDB.
returns:
dict(): {"last": str(last_entry),
"details": list(detailed_table),
"watthours": str(watthours)}
'''
if 10.5 <= direction < 34.5:
result = "NNE"
elif 34.5 <= direction < 58.5:
result = "NE"
elif 58.5 <= direction < 82.5:
result = "ENE"
elif 82.5 <= direction < 106.5:
result = "E"
elif 106.5 <= direction < 130.5:
result = "ESE"
elif 130.5 <= direction < 154.5:
result = "SE"
elif 154.5 <= direction < 178.5:
result = "SSE"
elif 178.5 <= direction < 202.5:
result = "S"
elif 202.5 <= direction < 226.5:
result = "SSW"
elif 226.5 <= direction < 250.5:
result = "SW"
elif 250.5 <= direction < 274.5:
result = "W"
elif 274.5 <= direction < 298.5:
result = "WNW"
elif 298.5 <= direction < 322.5:
result = "NW"
elif 322.5 <= direction < 346.5:
result = "NNW"
elif 346.5 <= direction <= 359.9 or 0 < direction < 10.5:
result = "N"
elif direction == 0:
result = "ERROR, windvan broken"
else:
result = "NaN"
return result
def LastKnownState(self):
'''
Function to get energy readings from InfluxDB.
returns:
dict(): {"last": str(last_entry),
"details": list(detailed_table),
"watthours": str(watthours)}
'''
influx_weather_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_weather_db
)
influx_status_client = influxdb.client.InfluxDBClient(
influx_host, influx_port, influx_user, influx_pwd, influx_status_db
)
# General query
query1 = "SELECT time, value FROM wind WHERE type = "
query2 = "AND time > NOW() - 5m ORDER BY time DESC LIMIT 1"
# now parse the tag in the middle of the two q from above
wind_speed_q = "{} '{}' {}".format(query1, "speed", query2)
wind_direction_q = "{} '{}' {}".format(query1, "direction", query2)
wind_gust_q = "{} '{}' {}".format(query1, "windgust", query2)
hum_ext_q = "SELECT time, humidity FROM temphumi WHERE type = 'external' {}".format(query2)
hum_int_q = "SELECT time, humidity FROM temphumi WHERE type = 'internal' {}".format(query2)
press_q = "SELECT time, pressure FROM temphumi WHERE type = 'adjusted' {}".format(query2)
presr_q = "SELECT time, pressure FROM temphumi WHERE type = 'raw' {}".format(query2)
t_ext_q = "SELECT time, temperature FROM temphumi WHERE type = 'external' {}".format(query2)
t_int_q = "SELECT time, temperature FROM temphumi WHERE type = 'internal' {}".format(query2)
# the actual query to DB
wind_speed = influx_weather_client.query(wind_speed_q)
wind_direction = influx_weather_client.query(wind_direction_q)
wind_gust = influx_weather_client.query(wind_gust_q)
hum_ext = influx_weather_client.query(hum_ext_q)
hum_int = influx_weather_client.query(hum_int_q)
press = influx_weather_client.query(press_q)
presr = influx_weather_client.query(presr_q)
t_ext = influx_weather_client.query(t_ext_q)
t_int = influx_weather_client.query(t_int_q)
# returned is a list, in this case, we just need one value [0]
result_windspeed = [speed for speed in wind_speed][0][0]
result_winddir = [direction for direction in wind_direction][0][0]
result_windgust = [gust for gust in wind_gust][0][0]
result_hum_int = [y for y in hum_int][0][0]
result_t_int = [c for c in t_int][0][0]
result_press = [z for z in press][0][0]
result_presr = [a for a in presr][0][0]
result_t_ext = [b for b in t_ext][0][0]
result_hum_ext = [x for x in hum_ext][0][0]
# Put the time to a uhman readable format, strip nanosecs
time_stamp = time.strptime(result_windspeed['time'].split('.')[0],
"%Y-%m-%dT%H:%M:%S")
result = {}
# Construct the result to return
result.update({"time": time_stamp})
result.update({"speed": round(result_windspeed['value'], 1)})
result.update({"direction": round(result_winddir['value'], 1)})
result.update({"windgust": round(result_windgust['value'], 1)})
result.update({"humidity_ext": round(result_hum_ext['humidity'], 1)})
result.update({"humidity_int": round(result_hum_int['humidity'], 1)})
result.update({"pressure": round(result_press['pressure'], 1)})
result.update({"pressure_raw": round(result_presr['pressure'], 1)})
result.update({"temp_ext": round(result_t_ext['temperature'], 1)})
result.update({"temp_int": round(result_t_int['temperature'], 1)})
return result
def body(self):
admin_preformat = read_html('weather_admin')
current_weather = self.LastKnownState()
admin_html = admin_preformat.format(
timestamp = time.strftime("%d.%m.%Y %H:%M:%S",
current_weather['time']),
w_speed_icon = fs_wind_icon,
w_speed_km = current_weather['speed'],
w_speed_ms = round(current_weather['speed'] / 3.6, 1),
w_gust_icon = fs_windgust_icon,
w_gust_km = current_weather['windgust'],
w_gust_ms = round(current_weather['windgust'] / 3.6, 1),
w_dir_icon = fs_winddirection_icon,
w_dir_name = self.winddirName(current_weather['direction']),
w_dir_deg = current_weather['direction'],
out_temp_icon = fs_out_temperature_icon,
out_temp = current_weather['temp_ext'],
in_temp_icon = fs_in_temperature_icon,
in_temp = current_weather['temp_int'],
pressure_icon = fs_pressure_icon,
pressure = current_weather['pressure'],
raw_pressure = current_weather['pressure_raw']
)
return admin_html