From 7ca173de3de046501d79164da0c8c8871a03089b Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Sat, 5 Mar 2016 16:47:32 +0100 Subject: web: o Adding an API method to get per-hour aggregate values. Doesn't use the by_hour table yet. o Adding a simple line graph component that can graph a single property's value. --- web/static/app/DillerRpc.js | 40 +++++- web/static/app/app.js | 179 ------------------------- web/static/app/diller/client.js | 72 ++++++++++ web/static/app/diller/global.js | 60 +++++++++ web/static/app/diller/line-chart.js | 62 +++++++++ web/static/app/diller/web.js | 236 +++++++++++++++++++++++++++++++++ web/static/app/templates/property.html | 30 +++-- web/templates/index.jade | 9 +- web/templates/wat.html | 2 +- 9 files changed, 491 insertions(+), 199 deletions(-) delete mode 100644 web/static/app/app.js create mode 100644 web/static/app/diller/client.js create mode 100644 web/static/app/diller/global.js create mode 100644 web/static/app/diller/line-chart.js create mode 100644 web/static/app/diller/web.js (limited to 'web') diff --git a/web/static/app/DillerRpc.js b/web/static/app/DillerRpc.js index f865dc5..eddbbf9 100644 --- a/web/static/app/DillerRpc.js +++ b/web/static/app/DillerRpc.js @@ -1,52 +1,82 @@ function DillerRpc($http, DillerConfig) { var baseUrl = DillerConfig.baseUrl; - function getDevices() { + function getDevicesReq() { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/device'; + return req; + } + + function getDevices() { + var req = getDevicesReq(); return $http(req); } - function getDevice(deviceId) { + function getDeviceReq(deviceId) { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/device/:deviceId'; req.url = req.url.replace(/:deviceId/, deviceId); + return req; + } + + function getDevice(deviceId) { + var req = getDeviceReq(deviceId); return $http(req); } - function patchDevice(deviceId, payload) { + function patchDeviceReq(deviceId, payload) { var req = {}; req.method = 'patch'; req.url = baseUrl + '/api/device/:deviceId'; req.url = req.url.replace(/:deviceId/, deviceId); req.data = payload; + return req; + } + + function patchDevice(deviceId, payload) { + var req = patchDeviceReq(deviceId, payload); return $http(req); } - function getValues(propertyId) { + function getValuesReq(propertyId) { var req = {}; req.method = 'get'; req.url = baseUrl + '/api/property/:propertyId/values'; req.url = req.url.replace(/:propertyId/, propertyId); + return req; + } + + function getValues(propertyId) { + var req = getValuesReq(propertyId); return $http(req); } - function patchProperty(propertyId, payload) { + function patchPropertyReq(propertyId, payload) { var req = {}; req.method = 'patch'; req.url = baseUrl + '/api/property/:propertyId/values'; req.url = req.url.replace(/:propertyId/, propertyId); req.data = payload; + return req; + } + + function patchProperty(propertyId, payload) { + var req = patchPropertyReq(propertyId, payload); return $http(req); } return { + getDevicesReq: getDevicesReq, getDevices: getDevices, + getDeviceReq: getDeviceReq, getDevice: getDevice, + patchDeviceReq: patchDeviceReq, patchDevice: patchDevice, + getValuesReq: getValuesReq, getValues: getValues, + patchPropertyReq: patchPropertyReq, patchProperty: patchProperty }; } diff --git a/web/static/app/app.js b/web/static/app/app.js deleted file mode 100644 index 22c7f83..0000000 --- a/web/static/app/app.js +++ /dev/null @@ -1,179 +0,0 @@ -(function () { - function FrontPageController(devices) { - var ctrl = this; - - ctrl.devices = devices.data.devices; - } - - function DeviceController($uibModal, device, DillerRpc) { - var ctrl = this; - - ctrl.device = device.data.device; - - ctrl.propertyChunks = _(ctrl.device.properties).sortByAll(['name', 'key']).chunk(3).value(); - - ctrl.editDeviceAttribute = function (attributeName) { - var outer = ctrl; - $uibModal.open({ - controller: function ($uibModalInstance) { - var ctrl = this; - - ctrl.title = 'Edit device ' + attributeName; - ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); - ctrl.value = outer.device[attributeName]; - - ctrl.update = function () { - DillerRpc.patchDevice(outer.device.id, {attribute: attributeName, value: ctrl.value}) - .then(function (res) { - outer.device = res.data.device; - $uibModalInstance.close({}); - }, function (res) { - ctrl.error = res.data.message; - }); - }; - }, - controllerAs: 'ctrl', - bindToController: true, - templateUrl: 'app/templates/edit-attribute.modal.html' - }); - } - } - - function PropertyController($timeout, $route, $uibModal, DillerRpc, device, values) { - var ctrl = this; - - function updateData(device) { - ctrl.device = device.data.device; - ctrl.property = _.find(ctrl.device.properties, {id: $route.current.params.propertyId}); - } - updateData(device); - ctrl.values = values.data.values; - - var refreshPromise; - ctrl.refresh = function () { - $timeout.cancel(refreshPromise); - refreshPromise = $timeout(function () { - ctrl.loading = true; - }, 200); - - DillerRpc.getValues($route.current.params.propertyId).then(function (res) { - ctrl.values = res.data.values; - ctrl.loading = false; - $timeout.cancel(refreshPromise); - }) - }; - - ctrl.editPropertyAttribute = function (attributeName) { - var outer = ctrl; - $uibModal.open({ - controller: function ($uibModalInstance) { - var ctrl = this; - - ctrl.title = 'Edit property ' + attributeName; - ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); - ctrl.value = outer.property[attributeName]; - - ctrl.update = function () { - DillerRpc.patchProperty(outer.property.id, {attribute: attributeName, value: ctrl.value}) - .then(function (res) { - updateData(res); - $uibModalInstance.close({}); - }, function (res) { - ctrl.error = res.data.message; - }); - }; - }, - controllerAs: 'ctrl', - bindToController: true, - templateUrl: 'app/templates/edit-attribute.modal.html' - }); - } - } - - function TimestampFilter() { - return function (value) { - if (!value) { - return; - } - - return moment(value).startOf('second').fromNow(); - } - } - - function DlTimestampDirective() { - return { - restrict: 'E', - scope: { - value: '=' - }, - replace: true, - template: '{{value|timestamp}}' - }; - } - - function DlDotsDirective() { - return { - restrict: 'E', - scope: { - value: '=' - }, - replace: true, - template: '...\n' - }; - } - - function config($routeProvider) { - $routeProvider - .when('/', { - controller: FrontPageController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/front-page.html', - resolve: { - devices: DillerRpcResolve.getDevices - } - }) - .when('/device/:deviceId', { - controller: DeviceController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/device.html', - resolve: { - device: DillerRpcResolve.getDevice - } - }) - .when('/device/:deviceId/property/:propertyId', { - controller: PropertyController, - controllerAs: 'ctrl', - templateUrl: 'app/templates/property.html', - resolve: { - device: DillerRpcResolve.getDevice, - values: DillerRpcResolve.getValues - } - }) - .otherwise({ - redirectTo: '/' - }); - } - - function run($log) { - window.console = $log; - } - - function DillerConfig() { - var head = document.getElementsByTagName('head')[0]; - var base = head.getElementsByTagName('base')[0]; - var baseUrl = base.href.replace(/\/$/, ''); - return { - baseUrl: baseUrl - }; - } - - angular - .module('Diller', ['ngRoute', 'ui.bootstrap']) - .config(config) - .run(run) - .filter('timestamp', TimestampFilter) - .directive('dlTimestamp', DlTimestampDirective) - .directive('dlDots', DlDotsDirective) - .service('DillerConfig', DillerConfig) - .service('DillerRpc', DillerRpc); -})(); diff --git a/web/static/app/diller/client.js b/web/static/app/diller/client.js new file mode 100644 index 0000000..6641ffb --- /dev/null +++ b/web/static/app/diller/client.js @@ -0,0 +1,72 @@ +(function () { + function extractData(res) { + return res && res.data; + } + + function Property($http, DillerRpc, propertyId) { + function getInterval(interval) { + + // moment().subtract(24, 'hour') + var req = DillerRpc.getValuesReq(propertyId); + req.params = { + from: interval.getFrom().toISOString() + }; + return $http(req).then(extractData); + } + + /** @lends Property.prototype */ + return { + getInterval: getInterval + }; + } + + function Device($http, DillerRpc, deviceId) { + var properties = {}; + + /** + * @param propertyId + * @returns {Property} + */ + function getProperty(propertyId) { + var p = properties[propertyId]; + + if (!p) { + p = new Property($http, DillerRpc, propertyId); + properties[propertyId] = p; + } + + return p; + } + + /** @lends Device.prototype */ + return { + getProperty: getProperty + }; + } + + function DillerClient($timeout, $http, DillerRpc) { + + var devices = {}; + + function getDevice(deviceId) { + var d = devices[deviceId]; + + if (!d) { + d = new Device($http, DillerRpc, deviceId); + devices[deviceId] = d; + } + + return d; + } + + /** @lends DillerClient.prototype */ + return { + getDevice: getDevice + }; + } + + angular + .module('diller.client', []) + .service('DillerClient', DillerClient) + .service('DillerRpc', window.DillerRpc); +})(); diff --git a/web/static/app/diller/global.js b/web/static/app/diller/global.js new file mode 100644 index 0000000..0da8da7 --- /dev/null +++ b/web/static/app/diller/global.js @@ -0,0 +1,60 @@ +(function () { + var Diller = window.Diller = window.Diller || {}; + + function toDate(unknown) { + if (!unknown) { + return undefined; + } else if (moment.isMoment(unknown)) { + return unknown; + } else if (typeof unknown === 'string') { + unknown = unknown.trim(); + + if (unknown == '') { + return moment(); + } + + return moment(unknown, 'YYYY-MM-DDTHH:mm.sssZ'); + } else if (typeof unknown === 'string' || moment.isDate(unknown)) { + return moment(unknown); + } else { + return undefined; + } + } + + Diller.Interval = function (from, to) { + var f = toDate(from), + t = toDate(to); + + if (f.isAfter(t)) { + var tmp = f; + f = t; + t = tmp; + } + + return { + getFrom: function () { + return f || moment(); + }, + getTo: function () { + return t || moment(); + }, + toString: function () { + return 'yo' + } + }; + }; + + Diller.Interval.create = function (value) { + if (value instanceof Diller.Interval) { + return value; + } + + return new Diller.Interval.hours(24); + }; + + Diller.Interval.hours = function (hours) { + var to = moment(); + var from = to.subtract(hours, 'hours'); + return new Diller.Interval(from, to); + }; +})(); diff --git a/web/static/app/diller/line-chart.js b/web/static/app/diller/line-chart.js new file mode 100644 index 0000000..69b6f77 --- /dev/null +++ b/web/static/app/diller/line-chart.js @@ -0,0 +1,62 @@ +(function () { + + var isoFormat = 'YYYY-MM-DDTHH:mm:ss'; + + function DlLineChartDirective($timeout, DillerClient) { + var id_seq = 0; + + return { + restrict: 'E', + scope: { + device: '=', + property: '=', + value: '=', + interval: '=' + }, + replace: true, + template: '
', + link: function (scope, element, attrs) { + var elementId = element.attr('id'); + if (!elementId) { + elementId = 'dl-line-chart-' + id_seq++; + element.attr('id', elementId); + } + + var deviceId = scope.device; + var propertyId = scope.property; + var interval = Diller.Interval.create(scope.interval); + var property = DillerClient.getDevice(deviceId).getProperty(propertyId); + + var options = { + axisX: { + showLabel: true, + showGrid: true, + labelInterpolationFnc: function (value, index) { + return index % 4 === 0 ? value.format('HH:mm') : null; + } + } + }; + + property.getInterval(interval).then(function (data) { + var avgs = _.pluck(data.values, 'avg'); + var timestamps = _.map(data.values, function (row) { + return moment(row.timestamp, isoFormat); + }); + + var chartData = { + labels: timestamps, + series: [ + avgs + ] + }; + + var chart = new Chartist.Line('#' + elementId, chartData, options); + }); + } + }; + } + + angular + .module('diller.line-chart', ['diller.client']) + .directive('dlLineChart', DlLineChartDirective); +})(); diff --git a/web/static/app/diller/web.js b/web/static/app/diller/web.js new file mode 100644 index 0000000..4b173ad --- /dev/null +++ b/web/static/app/diller/web.js @@ -0,0 +1,236 @@ +(function () { + var isoFormat = 'YYYY-MM-DDTHH:mm:ss'; + + function FrontPageController(devices) { + var ctrl = this; + + ctrl.devices = devices.data.devices; + } + + function DeviceController($uibModal, device, DillerRpc) { + var ctrl = this; + + ctrl.device = device.data.device; + + ctrl.propertyChunks = _(ctrl.device.properties).sortByAll(['name', 'key']).chunk(3).value(); + + ctrl.editDeviceAttribute = function (attributeName) { + var outer = ctrl; + $uibModal.open({ + controller: function ($uibModalInstance) { + var ctrl = this; + + ctrl.title = 'Edit device ' + attributeName; + ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); + ctrl.value = outer.device[attributeName]; + + ctrl.update = function () { + DillerRpc.patchDevice(outer.device.id, {attribute: attributeName, value: ctrl.value}) + .then(function (res) { + outer.device = res.data.device; + $uibModalInstance.close({}); + }, function (res) { + ctrl.error = res.data.message; + }); + }; + }, + controllerAs: 'ctrl', + bindToController: true, + templateUrl: 'app/templates/edit-attribute.modal.html' + }); + } + } + + function PropertyController($timeout, $route, $uibModal, DillerRpc, device, values) { + var ctrl = this; + + function updateData(device) { + ctrl.device = device.data.device; + ctrl.property = _.find(ctrl.device.properties, {id: $route.current.params.propertyId}); + } + updateData(device); + ctrl.values = values.data.values; + + var refreshPromise; + ctrl.refresh = function () { + $timeout.cancel(refreshPromise); + refreshPromise = $timeout(function () { + ctrl.loading = true; + }, 200); + + DillerRpc.getValues($route.current.params.propertyId).then(function (res) { + ctrl.values = res.data.values; + ctrl.loading = false; + $timeout.cancel(refreshPromise); + }) + }; + + ctrl.editPropertyAttribute = function (attributeName) { + var outer = ctrl; + $uibModal.open({ + controller: function ($uibModalInstance) { + var ctrl = this; + + ctrl.title = 'Edit property ' + attributeName; + ctrl.label = attributeName.substr(0, 1).toUpperCase() + attributeName.substr(1); + ctrl.value = outer.property[attributeName]; + + ctrl.update = function () { + DillerRpc.patchProperty(outer.property.id, {attribute: attributeName, value: ctrl.value}) + .then(function (res) { + updateData(res); + $uibModalInstance.close({}); + }, function (res) { + ctrl.error = res.data.message; + }); + }; + }, + controllerAs: 'ctrl', + bindToController: true, + templateUrl: 'app/templates/edit-attribute.modal.html' + }); + }; + + ctrl.interval = Diller.Interval.hours(5); + + // ctrl.values24h = values24h.data.values; + + function chartist() { + var avgs = _.pluck(ctrl.values24h, 'avg'); + var timestamps = _.map(ctrl.values24h, function(row) { + var m = moment(row.timestamp, isoFormat); + return m.format('HH:mm'); + }); + + var data = { + labels: timestamps, + series: [ + avgs + ] + }; + + var options = { + axisX: { + showLabel: false, + showGrid: false + } + }; + +// options.lineSmooth = Chartist.Interpolation.cardinal({ +// fillHoles: true, +// }) + + options.axisX = { + showLabel: true, + showGrid: true, + labelInterpolationFnc: function(value, index) { + return index % 4 === 0 ? value : null; + } + }; + + var chartData = { + series: [[]], + labels: [] + }; + + // var chart = new Chartist.Line('#values-chart', chartData, options); + // console.log('chart', chart); +/* + $timeout(function() { + chartData = data; + // console.log(data); + var chart = new Chartist.Line('#values-chart', chartData, options); + console.log('chart', chart); + }); +*/ + } + + // chartist(); + } + + function TimestampFilter() { + return function (value) { + if (!value) { + return; + } + + return moment(value).startOf('second').fromNow(); + } + } + + function DlTimestampDirective() { + return { + restrict: 'E', + scope: { + value: '=' + }, + replace: true, + template: '{{value|timestamp}}' + }; + } + + function DlDotsDirective() { + return { + restrict: 'E', + scope: { + value: '=' + }, + replace: true, + template: '...\n' + }; + } + + function config($routeProvider) { + $routeProvider + .when('/', { + controller: FrontPageController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/front-page.html', + resolve: { + devices: DillerRpcResolve.getDevices + } + }) + .when('/device/:deviceId', { + controller: DeviceController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/device.html', + resolve: { + device: DillerRpcResolve.getDevice + } + }) + .when('/device/:deviceId/property/:propertyId', { + controller: PropertyController, + controllerAs: 'ctrl', + templateUrl: 'app/templates/property.html', + resolve: { + device: DillerRpcResolve.getDevice, + values: DillerRpcResolve.getValues + } + }) + .otherwise({ + redirectTo: '/' + }); + } + + function run($log) { + window.console = $log; + } + + function DillerConfig() { + var head = document.getElementsByTagName('head')[0]; + var base = head.getElementsByTagName('base')[0]; + var baseUrl = base.href.replace(/\/$/, ''); + return { + baseUrl: baseUrl + }; + } + + angular + .module('diller.web', ['ngRoute', 'ui.bootstrap', 'diller.line-chart']) + .config(config) + .run(run) + .filter('timestamp', TimestampFilter) + .directive('dlTimestamp', DlTimestampDirective) + .directive('dlDots', DlDotsDirective) + .service('DillerConfig', DillerConfig); +})(); diff --git a/web/static/app/templates/property.html b/web/static/app/templates/property.html index 969734b..3f3424f 100644 --- a/web/static/app/templates/property.html +++ b/web/static/app/templates/property.html @@ -20,20 +20,25 @@

-
-
Key
-
- {{ctrl.property.key}} -   -
+
+
+
Key
+
+ {{ctrl.property.key}} +   +
-
Created
-
- {{ctrl.property.created_timestamp | date:'medium'}} -   -
-
+
Created
+
+ {{ctrl.property.created_timestamp | date:'medium'}} +   +
+
+
+ + + diff --git a/web/templates/index.jade b/web/templates/index.jade index 1b2adbc..84c31dc 100644 --- a/web/templates/index.jade +++ b/web/templates/index.jade @@ -17,14 +17,19 @@ html(lang='en') script(src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js", type="application/javascript") script(src="bower_components/lodash/lodash.js", type="application/javascript") script(src="bower_components/moment/moment.js", type="application/javascript") + script(src="bower_components/chartist/dist/chartist.js", type="application/javascript") script(src="app/DillerRpc.js", type="application/javascript") - script(src="app/app.js", type="application/javascript") + script(src="app/diller/global.js", type="application/javascript") + script(src="app/diller/client.js", type="application/javascript") + script(src="app/diller/web.js", type="application/javascript") + script(src="app/diller/line-chart.js", type="application/javascript") link(href="app/app.css", rel="stylesheet") link(rel="stylesheet", href="bower_components/font-awesome/css/font-awesome.min.css") + link(rel="stylesheet", href="bower_components/chartist/dist/chartist.css") - body(ng-app="Diller") + body(ng-app="diller.web") .container nav.navbar.navbar-dark.bg-inverse a.navbar-brand(href='#/') Diller diff --git a/web/templates/wat.html b/web/templates/wat.html index fd504a7..0e77077 100644 --- a/web/templates/wat.html +++ b/web/templates/wat.html @@ -17,7 +17,7 @@ - + Loading Diller ... -- cgit v1.2.3