Also, get basic data being loaded, and get the fetching to work somewhat ok. Needed to deal w/ the fact that HighStocks does stupid requests data... Issues still present: not timezone aware caps between days in the data not all navigator series are barsmain
@@ -3,6 +3,7 @@ solardash.egg-info | |||||
p | p | ||||
*.pyc | *.pyc | ||||
.coverage | .coverage | ||||
fakedata.js | |||||
root/js/solardash.file.js | root/js/solardash.file.js | ||||
root/js/solardash.https.js | root/js/solardash.https.js | ||||
root/js/solardash.http.js | root/js/solardash.http.js |
@@ -14,10 +14,21 @@ FILES=$(PROJNAME)/__init__.py | |||||
JSFILES = root/js/solardash.file.js root/js/solardash.https.js root/js/solardash.http.js | JSFILES = root/js/solardash.file.js root/js/solardash.https.js root/js/solardash.http.js | ||||
THIRDPARTYJS = root/js/jquery.js root/js/highstock.js | |||||
THIRDPARTYJS = \ | |||||
root/js/jquery.js \ | |||||
root/js/highstock.js \ | |||||
# root/js/moment.min.js \ | |||||
# root/js/moment-timezone-with-data.min.js | |||||
root/js/jquery.js: | root/js/jquery.js: | ||||
wget -O $@ "https://code.jquery.com/jquery-3.4.1.min.js" | |||||
wget -O $@ "https://code.jquery.com/jquery-3.4.1.min.js" || (rm "$@"; false) | |||||
root/js/moment.min.js: | |||||
wget -O $@ "https://momentjs.com/downloads/moment.min.js" || (rm "$@"; false) | |||||
root/js/moment-timezone-with-data.min.js: | |||||
wget -O $@ "https://momentjs.com/downloads/moment-timezone-with-data-1970-2030.js" || (rm "$@"; false) | |||||
root/js/highstock.js: Makefile | root/js/highstock.js: Makefile | ||||
wget -O - "https://code.highcharts.com/stock/8.0.0/highstock.js" | grep -v '^//# sourceMappingURL=' > $@ || (rm "$@"; false) | wget -O - "https://code.highcharts.com/stock/8.0.0/highstock.js" | grep -v '^//# sourceMappingURL=' > $@ || (rm "$@"; false) | ||||
@@ -36,8 +47,15 @@ run: $(JSFILES) | |||||
$(JSFILES): $(THIRDPARTYJS) $(JSBASE) | $(JSFILES): $(THIRDPARTYJS) $(JSBASE) | ||||
root/js/solardash.file.js: fakedata.js | |||||
fakedata.js: fakedata.py | |||||
python $< > $@ || (rm $@; false) | |||||
.jspp.js: | .jspp.js: | ||||
cat $< $(THIRDPARTYJS) $(JSBASE) > $@ || (rm "$@"; false) | |||||
# bsdmake uses $>, gmake uses $^ | |||||
(echo '// DO NOT EDIT FILE!!!! THIS IS AUTOMATICALLY GENERATED!!!'; cat $^) > $@ || (rm "$@"; false) | |||||
#cat $< $(THIRDPARTYJS) $(JSBASE) > $@ || (rm $@; false) | |||||
keepupdate: | keepupdate: | ||||
find . -name '*.js' -o -name '*.jspp' | entr make all | find . -name '*.js' -o -name '*.jspp' | entr make all | ||||
@@ -52,6 +52,14 @@ Maybe heatmap for solar panels | |||||
Use removePoint and addPoint: https://web.archive.org/web/20200109083308/https://api.highcharts.com/class-reference/Highcharts.Series.html | Use removePoint and addPoint: https://web.archive.org/web/20200109083308/https://api.highcharts.com/class-reference/Highcharts.Series.html | ||||
to add/remove the end null point and add points as they dynamically arrive | to add/remove the end null point and add points as they dynamically arrive | ||||
Sample Home power Consumption dataset: | |||||
https://archive.ics.uci.edu/ml/datasets/individual+household+electric+power+consumption | |||||
List of Solar resources: | |||||
https://energydemo.github.io/SolarDatasets/ | |||||
https://pvoutput.org | |||||
WebSocket Definition | WebSocket Definition | ||||
-------------------- | -------------------- | ||||
@@ -73,6 +81,13 @@ Messages from the websocket: | |||||
ov overview of data, contains gird, production and consumption data. | ov overview of data, contains gird, production and consumption data. | ||||
following is JSON object | following is JSON object | ||||
win The graph has set a window size. The two arguments are start and end. If this is the | |||||
first time, NaN will be sent for both, which means to send all data. | |||||
windata Data in response to a window command. The format is an object w/ the keys, production, | |||||
grid and consumption. Each will be a list of data points. Each point is a pair of | |||||
timestamp in miliseconds and the value for the point. | |||||
One the first connection, the following messages/data will be sent: | One the first connection, the following messages/data will be sent: | ||||
c, ng, p, ov | c, ng, p, ov | ||||
@@ -2,3 +2,13 @@ Solar Dashboard | |||||
=============== | =============== | ||||
This is a solar dashboard for displaying information about a solar install. | This is a solar dashboard for displaying information about a solar install. | ||||
Structure | |||||
--------- | |||||
Frontend | |||||
The front end is located in root. The meat of the logic is in | |||||
`js/solardash.base.js`. The testing infrastructure is in | |||||
`js/solardash.file.js` as if you are launching the page from a file | |||||
url, there is no way to make a url to access the backend. |
@@ -0,0 +1 @@ | |||||
ls fakedata.py | entr sh -c 'python fakedata.py > fake.txt; echo plot \"fake.txt\" | gnuplot' |
@@ -0,0 +1,133 @@ | |||||
import arrow | |||||
import random | |||||
import pprint | |||||
import json | |||||
from datetime import datetime, timedelta | |||||
rand = random.Random('a seed') | |||||
tz = 'US/Pacific' | |||||
startdate = arrow.Arrow(2019, 11, 1, tzinfo=tz) | |||||
enddate = arrow.Arrow(2019, 12, 10, tzinfo=tz) | |||||
meanwhprod = 20000 | |||||
sigwhprod = 2000 | |||||
def drange(s, e, interval): | |||||
cmpfun = lambda s, e, ts, te: te < e | |||||
if e < s: | |||||
interval = -interval | |||||
cmpfun = lambda s, e, ts, te: te > e | |||||
ts = s.clone() | |||||
te = ts + interval | |||||
#print('dr:', repr((s, e, ts, te, cmpfun(s, e, ts, te), interval))) | |||||
while cmpfun(s, e, ts, te): | |||||
yield ts + (te - ts) | |||||
ts, te = te, te + interval | |||||
# idea: | |||||
# first hour linear ramp up (25% in first half, 75% in second half) | |||||
# middle 5 hours near constant generation | |||||
# first/tailing linear is equiv of an hour total, so total power / 6 | |||||
# approx 7 hours time | |||||
def makestartend(t): | |||||
s = t.replace(hour=9).shift(minutes=rand.gauss(30, 10)) | |||||
e = t.replace(hour=16).shift(minutes=rand.gauss(30, 10)) | |||||
return (s, e) | |||||
def normdist(small, big, amount, minsize): | |||||
'''Distribute most twoards the big side, total distribute amount | |||||
over the entire range, [small, big]. | |||||
''' | |||||
ret = [] | |||||
timediff = abs(big - small) | |||||
if timediff < minsize: | |||||
scaledamount = amount * (timedelta(hours=1) / timediff) | |||||
midpnt = small + (big - small) / 2 | |||||
#print('ndf:', repr((small, big, midpnt, amount, scaledamount))) | |||||
return [ (midpnt, scaledamount) ] | |||||
#print('nd:', repr((small, big, amount))) | |||||
dist = big - small | |||||
halfpoint = small + (dist / 9 * 5) | |||||
ret.extend(normdist(small, halfpoint, amount / 2, minsize)) | |||||
ret.extend(normdist(halfpoint, big, amount / 2, minsize)) | |||||
#print('ndr:') | |||||
#pprint.pprint(ret) | |||||
return ret | |||||
def linramp(start, end, wtarget, minsize): | |||||
mid = start + (end - start) / 2 | |||||
timediff = abs(end - start) | |||||
ret = [] | |||||
ndates = abs((end - start) / minsize) | |||||
#print('lr', ndates) | |||||
for x, i in enumerate(drange(start, end, minsize)): | |||||
#print(repr((x, i))) | |||||
yield (i, (x + 1) / ndates * wtarget) | |||||
def distribute(s, e, prod, minsize): | |||||
onehour = timedelta(hours=1) | |||||
totaltime = e - s | |||||
mid = s + totaltime / 2 | |||||
startrampend = s + onehour | |||||
endrampstart = e - onehour | |||||
# prod == wh | |||||
wtarget = prod / ((totaltime + onehour).seconds / 60 / 60) | |||||
ret = [] | |||||
#print('d:', repr((s, e))) | |||||
ret.extend(linramp(s, startrampend, wtarget, minsize)) | |||||
for i in drange(startrampend, endrampstart, minsize): | |||||
ret.append((i, wtarget)) | |||||
ret.extend(linramp(e, endrampstart, wtarget, minsize)) | |||||
ret.sort() | |||||
#pprint.pprint(ret) | |||||
return ret | |||||
#print('start') | |||||
points = [] | |||||
index = [] | |||||
def serializearrowasmili(obj): | |||||
if not isinstance(obj, arrow.Arrow): | |||||
raise TypeError | |||||
return int(obj.float_timestamp*1000) | |||||
for i in arrow.Arrow.range('day', startdate, enddate): | |||||
whprod = rand.gauss(meanwhprod, sigwhprod) | |||||
s, e = makestartend(i) | |||||
noon = i.replace(hour=12) | |||||
index.append((noon, whprod)) | |||||
#print(repr(i), whprod) | |||||
dist = distribute(s, e, whprod, timedelta(seconds=20)) | |||||
# print timestamps as miliseconds | |||||
if False: | |||||
dist = ((int(a.float_timestamp*1000), b) for a, b in dist) | |||||
# print space delimited time, else json | |||||
if False: | |||||
print('\n'.join('%s %s' % (a, b) for a, b in dist)) | |||||
else: | |||||
#print(json.dumps(tuple(dist), indent=2)) | |||||
points.extend(dist) | |||||
print('fakedata =', json.dumps(dict(production=points, index=index), default=serializearrowasmili)) |
@@ -1,3 +1,9 @@ | |||||
//Highcharts.setOptions({ | |||||
// time: { | |||||
// timezone: 'America/Los_Angeles' | |||||
// } | |||||
//}); | |||||
socket.onopen = function() { | socket.onopen = function() { | ||||
// connected | // connected | ||||
} | } | ||||
@@ -20,6 +26,19 @@ function netgridcolor(v) { | |||||
return "#ff0000"; | return "#ff0000"; | ||||
} | } | ||||
function afterSetExtremes(e) { | |||||
// hack to deal w/ HighStocks being stupid | |||||
if (Date.now() - solarchart.sd_lastwindata < 500) { | |||||
// Make sure we trigger again, and that the user doesn't | |||||
// have to wait too long | |||||
solarchart.sd_lastwindata = Date.now() - 500; | |||||
return; | |||||
} | |||||
solarchart.showLoading('Loading data from server...'); | |||||
console.log(e) | |||||
socket.send('win ' + Math.round(e.min).toString() + ' ' + Math.round(e.max).toString()); | |||||
} | |||||
socket.onmessage = function(m) { | socket.onmessage = function(m) { | ||||
var msg = m.data.split(" "); | var msg = m.data.split(" "); | ||||
if (msg[0] in msgNumbs) { | if (msg[0] in msgNumbs) { | ||||
@@ -40,6 +59,15 @@ socket.onmessage = function(m) { | |||||
solarchart.navigator.series[gridIdx].setData(data.grid, false); | solarchart.navigator.series[gridIdx].setData(data.grid, false); | ||||
solarchart.navigator.series[prodIdx].setData(data.production, false); | solarchart.navigator.series[prodIdx].setData(data.production, false); | ||||
solarchart.redraw(); | solarchart.redraw(); | ||||
} else if (msg[0] == 'windata') { | |||||
var data = JSON.parse(msg[1]); | |||||
console.log(data); | |||||
solarchart.sd_lastwindata = Date.now(); | |||||
solarchart.series[consumIdx].setData(data.consumption, false); | |||||
//solarchart.navigator.series[gridIdx].setData(data.grid, false); | |||||
//solarchart.navigator.series[prodIdx].setData(data.production, false); | |||||
solarchart.redraw(); | |||||
solarchart.hideLoading() | |||||
} | } | ||||
} | } | ||||
@@ -59,16 +87,19 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||||
adaptToUpdatedData: false, // XXX - keep? | adaptToUpdatedData: false, // XXX - keep? | ||||
series: [ | series: [ | ||||
{ | { | ||||
type: 'bar', | |||||
name: 'consumptionNav', | name: 'consumptionNav', | ||||
showInNavigator: true, | showInNavigator: true, | ||||
data: [], | data: [], | ||||
}, | }, | ||||
{ | { | ||||
type: 'bar', | |||||
name: 'gridNav', | name: 'gridNav', | ||||
showInNavigator: true, | showInNavigator: true, | ||||
data: [], | data: [], | ||||
}, | }, | ||||
{ | { | ||||
type: 'bar', | |||||
name: 'productionNav', | name: 'productionNav', | ||||
showInNavigator: true, | showInNavigator: true, | ||||
data: [], | data: [], | ||||
@@ -76,6 +107,9 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||||
] | ] | ||||
}, | }, | ||||
xAxis: { | xAxis: { | ||||
events: { | |||||
afterSetExtremes: afterSetExtremes, | |||||
}, | |||||
type: "datetime", | type: "datetime", | ||||
title: { | title: { | ||||
enabled: false | enabled: false | ||||
@@ -83,12 +117,12 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||||
}, | }, | ||||
yAxis: { | yAxis: { | ||||
title: { | title: { | ||||
text: 'kW' | |||||
text: 'W' | |||||
} | } | ||||
}, | }, | ||||
tooltip: { | tooltip: { | ||||
split: true, | split: true, | ||||
valueSuffix: ' kW' | |||||
valueSuffix: ' W' | |||||
}, | }, | ||||
plotOptions: { | plotOptions: { | ||||
area: { | area: { | ||||
@@ -104,7 +138,10 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||||
series: [ | series: [ | ||||
{ | { | ||||
name: 'grid', | name: 'grid', | ||||
data: fakeData, | |||||
type: 'area', | |||||
gapSize: 1, | |||||
//gapUnit: 'value', | |||||
data: [ ], | |||||
}, | }, | ||||
] | ] | ||||
}); | }); |
@@ -7,7 +7,45 @@ function WebSocketTest(actions) { | |||||
this.processNextItem() | this.processNextItem() | ||||
} | } | ||||
// WebSocketTest.prototype.send = function () | |||||
Array.prototype.bisect = function (val, lo, hi) { | |||||
var mid; | |||||
if (lo == null) | |||||
lo = 0; | |||||
if (hi == null) | |||||
hi = this.length; | |||||
while (lo < hi) { | |||||
mid = Math.floor((lo + hi) / 2); | |||||
if (this[mid] < val) | |||||
lo = mid + 1; | |||||
else | |||||
hi = mid; | |||||
} | |||||
return lo; | |||||
}; | |||||
WebSocketTest.prototype.send = function (s) { | |||||
console.log("ws send: " + s); | |||||
var msg = s.split(" "); | |||||
if (msg[0] == 'win') { | |||||
if (msg[1] == 'NaN') | |||||
msg[1] = -Infinity; | |||||
if (msg[2] == 'NaN') | |||||
msg[2] = Infinity; | |||||
var loidx, hiidx; | |||||
loidx = fakedata.production.bisect([msg[1], 0]); | |||||
hiidx = fakedata.production.bisect([msg[2], 0]); | |||||
var subar = fakedata.production.slice(loidx, hiidx + 1); | |||||
var data = { | |||||
"production": subar, | |||||
"consumption": subar, | |||||
"grid": subar, | |||||
} | |||||
this.makercv('windata ' + JSON.stringify(data)); | |||||
} | |||||
} | |||||
// WebSocketTest.prototype.close = function () | // WebSocketTest.prototype.close = function () | ||||
// Internal | // Internal | ||||
@@ -26,6 +64,9 @@ WebSocketTest.prototype.processNextItem = function () { | |||||
return this; | return this; | ||||
} | } | ||||
WebSocketTest.prototype.makercv = function (m) { | |||||
this.onmessage(new MessageEvent('websockettest', { data: m })); | |||||
} | |||||
fakeData = [ | fakeData = [ | ||||
[ 1578544199000, 0.3934 ], | [ 1578544199000, 0.3934 ], | ||||
@@ -59,26 +100,21 @@ fakeOverviewData = { | |||||
production: fakeConData, | production: fakeConData, | ||||
} | } | ||||
function getoverviewdata() { | |||||
return { | |||||
grid: fakedata.index, | |||||
consumption: fakedata.index, | |||||
production: fakedata.index, | |||||
} | |||||
} | |||||
// Setup the socket that will be used | // Setup the socket that will be used | ||||
var socket = new WebSocketTest([ | var socket = new WebSocketTest([ | ||||
[ 10, function(a) { a.onopen(new Object()) } ], | [ 10, function(a) { a.onopen(new Object()) } ], | ||||
[ 10, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'o ' + JSON.stringify(fakeOverviewData) | |||||
})) } ], | |||||
[ 10, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'p .123' | |||||
})) } ], | |||||
[ 10, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'c .302' | |||||
})) } ], | |||||
[ 10, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'ng .758' | |||||
})) } ], | |||||
[ 2000, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'p .234' | |||||
})) } ], | |||||
[ 10, function(a) { a.onmessage(new MessageEvent('websockettest', { | |||||
data : 'ng -.584' | |||||
})) } ], | |||||
[ 10, function(a) { a.makercv('o ' + JSON.stringify(getoverviewdata())) } ], | |||||
[ 10, function(a) { a.makercv('p .123') } ], | |||||
[ 10, function(a) { a.makercv('c .302') } ], | |||||
[ 10, function(a) { a.makercv('ng .758') } ], | |||||
[ 2000, function(a) { a.makercv('p .234') } ], | |||||
[ 10, function(a) { a.makercv('ng -.584') } ], | |||||
]); | ]); |