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 | |||
| *.pyc | |||
| .coverage | |||
| fakedata.js | |||
| root/js/solardash.file.js | |||
| root/js/solardash.https.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 | |||
| 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: | |||
| 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 | |||
| 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) | |||
| root/js/solardash.file.js: fakedata.js | |||
| fakedata.js: fakedata.py | |||
| python $< > $@ || (rm $@; false) | |||
| .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: | |||
| 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 | |||
| 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 | |||
| -------------------- | |||
| @@ -73,6 +81,13 @@ Messages from the websocket: | |||
| ov overview of data, contains gird, production and consumption data. | |||
| 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: | |||
| c, ng, p, ov | |||
| @@ -2,3 +2,13 @@ Solar Dashboard | |||
| =============== | |||
| 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() { | |||
| // connected | |||
| } | |||
| @@ -20,6 +26,19 @@ function netgridcolor(v) { | |||
| 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) { | |||
| var msg = m.data.split(" "); | |||
| if (msg[0] in msgNumbs) { | |||
| @@ -40,6 +59,15 @@ socket.onmessage = function(m) { | |||
| solarchart.navigator.series[gridIdx].setData(data.grid, false); | |||
| solarchart.navigator.series[prodIdx].setData(data.production, false); | |||
| 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? | |||
| series: [ | |||
| { | |||
| type: 'bar', | |||
| name: 'consumptionNav', | |||
| showInNavigator: true, | |||
| data: [], | |||
| }, | |||
| { | |||
| type: 'bar', | |||
| name: 'gridNav', | |||
| showInNavigator: true, | |||
| data: [], | |||
| }, | |||
| { | |||
| type: 'bar', | |||
| name: 'productionNav', | |||
| showInNavigator: true, | |||
| data: [], | |||
| @@ -76,6 +107,9 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||
| ] | |||
| }, | |||
| xAxis: { | |||
| events: { | |||
| afterSetExtremes: afterSetExtremes, | |||
| }, | |||
| type: "datetime", | |||
| title: { | |||
| enabled: false | |||
| @@ -83,12 +117,12 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||
| }, | |||
| yAxis: { | |||
| title: { | |||
| text: 'kW' | |||
| text: 'W' | |||
| } | |||
| }, | |||
| tooltip: { | |||
| split: true, | |||
| valueSuffix: ' kW' | |||
| valueSuffix: ' W' | |||
| }, | |||
| plotOptions: { | |||
| area: { | |||
| @@ -104,7 +138,10 @@ var solarchart = Highcharts.stockChart('solarchart', { | |||
| series: [ | |||
| { | |||
| name: 'grid', | |||
| data: fakeData, | |||
| type: 'area', | |||
| gapSize: 1, | |||
| //gapUnit: 'value', | |||
| data: [ ], | |||
| }, | |||
| ] | |||
| }); | |||
| @@ -7,7 +7,45 @@ function WebSocketTest(actions) { | |||
| 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 () | |||
| // Internal | |||
| @@ -26,6 +64,9 @@ WebSocketTest.prototype.processNextItem = function () { | |||
| return this; | |||
| } | |||
| WebSocketTest.prototype.makercv = function (m) { | |||
| this.onmessage(new MessageEvent('websockettest', { data: m })); | |||
| } | |||
| fakeData = [ | |||
| [ 1578544199000, 0.3934 ], | |||
| @@ -59,26 +100,21 @@ fakeOverviewData = { | |||
| production: fakeConData, | |||
| } | |||
| function getoverviewdata() { | |||
| return { | |||
| grid: fakedata.index, | |||
| consumption: fakedata.index, | |||
| production: fakedata.index, | |||
| } | |||
| } | |||
| // Setup the socket that will be used | |||
| var socket = new WebSocketTest([ | |||
| [ 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') } ], | |||
| ]); | |||