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

2
web/TODO.txt Executable file
View File

@@ -0,0 +1,2 @@
Absolute web path throughout the project
Graphs share JS variables, separate them.

50
web/chttpd.py Executable file
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()

497
web/dynamic.py Executable file
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
web/static/css/bootstrap-grid.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

1
web/static/css/bootstrap-grid.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

459
web/static/css/bootstrap-reboot.css vendored Normal file
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

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 */

View File

@@ -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
web/static/css/bootstrap.css vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

6
web/static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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;
}

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
web/static/css/dygraph.css Normal file
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);
}

BIN
web/static/img/53096.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

52
web/static/img/53096.svg Normal file
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

BIN
web/static/img/53096_s.jpg Normal file

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
web/static/js/bootstrap.js vendored Normal file

File diff suppressed because it is too large Load Diff

7
web/static/js/bootstrap.min.js vendored Normal file

File diff suppressed because one or more lines are too long

9383
web/static/js/dygraph.js Normal file

File diff suppressed because it is too large Load Diff

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)
}
}())

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();
}

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);
}

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);
}

106
web/status.py Executable file
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

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
web/templates/header.html Normal file
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>

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>

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>

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>

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>

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>

317
web/voltage.py Executable file
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

200
web/weather.py Executable file
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