aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Barnett <james.barnett@fivium.co.uk>2018-04-08 17:25:47 +0100
committerGitHub <noreply@github.com>2018-04-08 17:25:47 +0100
commiteb0185c03f8d5b68949798e8040b42a203b040a2 (patch)
tree86da8449b0c92ec29a15baffe3e7bd8ebd34488e
parent954aff49707738e660e92e0418c31b1ec78a85a8 (diff)
parent4ec37af74fc77f7381d7c5c3b2560c8726f75ffa (diff)
downloadtplink-energy-monitor-eb0185c03f8d5b68949798e8040b42a203b040a2.tar.xz
tplink-energy-monitor-eb0185c03f8d5b68949798e8040b42a203b040a2.zip
Merge pull request #1 from jamesbarnett91/websocketsv0.2
Switch from http polling to websockets
-rw-r--r--README.md6
-rw-r--r--app.js31
-rwxr-xr-xbin/www90
-rw-r--r--package.json7
-rw-r--r--public/javascripts/dash.js167
-rw-r--r--public/stylesheets/style.css63
-rw-r--r--routes/energy-usage.js166
-rw-r--r--routes/index.js20
-rw-r--r--routes/power-state.js23
-rw-r--r--routes/ws.js28
-rw-r--r--services/data-broadcaster.js43
-rw-r--r--services/data-fetcher.js277
-rw-r--r--services/device-manager.js12
-rw-r--r--views/index.hbs38
14 files changed, 552 insertions, 419 deletions
diff --git a/README.md b/README.md
index e1fa2e2..fd28b6a 100644
--- a/README.md
+++ b/README.md
@@ -51,10 +51,10 @@ If you hit this issue you can try disabling the VirtualBox adapter in `Control P
- [x] Show historical data
- [x] Build dists
- [x] Docker image
-- [ ] Support switching between multiple plugs (currently only works for the fist plug discovered)
+- [x] Support switching between multiple plugs
+- [x] Switch to websockets
- [ ] Rescan for devices on the fly
- [ ] Add daily cost metrics
-- [ ] Configurable poll rates
-- [ ] Switch to websockets
+
diff --git a/app.js b/app.js
index 5512f07..1d24419 100644
--- a/app.js
+++ b/app.js
@@ -1,14 +1,14 @@
-var createError = require('http-errors');
-var express = require('express');
-var path = require('path');
-var cookieParser = require('cookie-parser');
-var logger = require('morgan');
+const createError = require('http-errors');
+const express = require('express');
+const path = require('path');
+const cookieParser = require('cookie-parser');
+const logger = require('morgan');
-var indexRouter = require('./routes/index');
-var energyUsageRouter = require('./routes/energy-usage');
-var powerStateRouter = require('./routes/power-state');
+const app = express();
+const expressWs = require('express-ws')(app);
-var app = express();
+const indexRouter = require('./routes/index');
+const wsRouter = require('./routes/ws');
// view engine setup
app.set('views', path.join(__dirname, 'views'));
@@ -21,8 +21,7 @@ app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
-app.use('/energy-usage', energyUsageRouter);
-app.use('/power-state', powerStateRouter);
+app.use('/ws', wsRouter);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
@@ -40,4 +39,12 @@ app.use(function(err, req, res, next) {
res.render('error');
});
-module.exports = app;
+app.listen(process.env.PORT || '3000');
+
+module.exports.getWsClientCount = function() {
+ return expressWs.getWss().clients.size;
+};
+
+module.exports.getWsClients = function() {
+ return expressWs.getWss().clients;
+};
diff --git a/bin/www b/bin/www
deleted file mode 100755
index 7356b2b..0000000
--- a/bin/www
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * Module dependencies.
- */
-
-var app = require('../app');
-var debug = require('debug')('tplink-monitor:server');
-var http = require('http');
-
-/**
- * Get port from environment and store in Express.
- */
-
-var port = normalizePort(process.env.PORT || '3000');
-app.set('port', port);
-
-/**
- * Create HTTP server.
- */
-
-var server = http.createServer(app);
-
-/**
- * Listen on provided port, on all network interfaces.
- */
-
-server.listen(port);
-server.on('error', onError);
-server.on('listening', onListening);
-
-/**
- * Normalize a port into a number, string, or false.
- */
-
-function normalizePort(val) {
- var port = parseInt(val, 10);
-
- if (isNaN(port)) {
- // named pipe
- return val;
- }
-
- if (port >= 0) {
- // port number
- return port;
- }
-
- return false;
-}
-
-/**
- * Event listener for HTTP server "error" event.
- */
-
-function onError(error) {
- if (error.syscall !== 'listen') {
- throw error;
- }
-
- var bind = typeof port === 'string'
- ? 'Pipe ' + port
- : 'Port ' + port;
-
- // handle specific listen errors with friendly messages
- switch (error.code) {
- case 'EACCES':
- console.error(bind + ' requires elevated privileges');
- process.exit(1);
- break;
- case 'EADDRINUSE':
- console.error(bind + ' is already in use');
- process.exit(1);
- break;
- default:
- throw error;
- }
-}
-
-/**
- * Event listener for HTTP server "listening" event.
- */
-
-function onListening() {
- var addr = server.address();
- var bind = typeof addr === 'string'
- ? 'pipe ' + addr
- : 'port ' + addr.port;
- debug('Listening on ' + bind);
-}
diff --git a/package.json b/package.json
index 41b6569..b97fe2e 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,7 @@
"name": "James Barnett"
},
"scripts": {
- "start": "node ./bin/www",
+ "start": "node ./app.js",
"dist": "pkg . --out-path ./dist"
},
"dependencies": {
@@ -19,13 +19,14 @@
"http-errors": "~1.6.2",
"morgan": "~1.9.0",
"tplink-smarthome-api": "0.22.0",
- "moment": "2.22.0"
+ "moment": "2.22.0",
+ "express-ws": "3.0.0"
},
"devDependencies": {
"pkg": "4.3.1"
},
"bin" : {
- "tplink-monitor": "./bin/www"
+ "tplink-monitor": "./app.js"
},
"pkg": {
"scripts": "node_modules/hbs/lib/hbs.js",
diff --git a/public/javascripts/dash.js b/public/javascripts/dash.js
index 43524ad..821e9c1 100644
--- a/public/javascripts/dash.js
+++ b/public/javascripts/dash.js
@@ -1,8 +1,5 @@
var dash = {
- realtimeUsagePollRateMs: 1000,
- powerStatePollRateMs: 60000,
- historicalStatsPollRateMs: 300000,
- pollingEnabled: true,
+ deviceId: null,
realtimeGauge: null,
realtimeTrendChart: null,
@@ -11,13 +8,59 @@ var dash = {
dailyUsageChart: null,
monthlyUsageChart: null,
- init: function() {
+ init: function(deviceId) {
+ this.deviceId = deviceId;
+
+ $('.' + deviceId).addClass('active');
+
this.initRealtimeGauge();
this.initRealtimeTrendChart();
this.initDailyUsageChart();
this.initMonthlyUsageChart();
- this.startPolling();
+ this.initWsConnection();
+ },
+
+ initWsConnection: function() {
+ var wsUri = 'ws://' + window.location.host + '/ws'
+ var ws = new WebSocket(wsUri);
+ ws.onopen = function () {
+ console.log('Websocket connection established');
+ $('#connection-error').hide(200);
+ ws.send(JSON.stringify(
+ {
+ requestType: 'getCachedData',
+ deviceId: dash.deviceId
+ }
+ ));
+ }
+ ws.onmessage = dash.wsMessageHandler;
+
+ ws.onclose = function() {
+ // Usually caused by mobile devices going to sleep or the user minimising the browser app.
+ // The setTimeout will begin once the device wakes from sleep or the browser regains focus.
+ $('#connection-error').show();
+ setTimeout(dash.initWsConnection, 2000);
+ }
+ },
+
+ wsMessageHandler: function(messageEvent) {
+ let message = JSON.parse(messageEvent.data);
+ if(message.deviceId === dash.deviceId) {
+ if(message.dataType === 'realtimeUsage') {
+ dash.refreshRealtimeDisplay(message.data);
+ }
+ else if(message.dataType === 'dailyUsage') {
+ dash.parseDailyUsageData(message.data);
+ }
+ else if(message.dataType === 'monthlyUsage') {
+ dash.parseMonthlyUsageData(message.data);
+ }
+ else if(message.dataType === 'powerState') {
+ dash.refreshPowerState(message.data);
+ }
+ }
+
},
initRealtimeGauge: function() {
@@ -81,6 +124,15 @@ var dash = {
tooltips: {
intersect: false
},
+ plugins: {
+ streaming: {
+ duration: 60000,
+ refresh: 1000,
+ delay: 1000,
+ frameRate: 30,
+ onRefresh: dash.realtimeTrendChartOnRefresh
+ }
+ }
}
});
},
@@ -156,22 +208,6 @@ var dash = {
});
},
- // TODO - should probably use websockets
- pollUsage: function() {
- if(this.pollingEnabled) {
- $.ajax({
- url: "/energy-usage/1/realtime",
- type: "GET",
- success: function(data) {
- dash.refreshRealtimeDisplay(data);
- },
- dataType: "json",
- complete: setTimeout(function() {dash.pollUsage()}, dash.realtimeUsagePollRateMs),
- timeout: 2000
- });
- }
- },
-
refreshRealtimeDisplay: function(realtime) {
var power = Math.round(realtime.power);
@@ -187,41 +223,6 @@ var dash = {
this.realtimeTrendLastSample = power;
},
- startPolling: function() {
- this.pollingEnabled = true;
- this.realtimeTrendChart.options.plugins.streaming = {
- duration: 60000,
- refresh: 1000,
- delay: 1000,
- frameRate: 30,
- onRefresh: dash.realtimeTrendChartOnRefresh
- };
-
- this.pollUsage();
- this.pollPowerStatus();
- this.pollDayStats();
- this.pollMonthStats();
- },
-
- stopPolling: function() {
- this.pollingEnabled = false;
- this.realtimeTrendChart.options.plugins.streaming = false;
- },
-
- pollDayStats: function() {
- if(this.pollingEnabled) {
- $.ajax({
- url: "/energy-usage/1/day-stats",
- type: "GET",
- success: function(data) {
- dash.parseDailyUsageData(data);
- },
- dataType: "json",
- complete: setTimeout(function() {dash.pollDayStats()}, dash.historicalStatsPollRateMs),
- timeout: 4000
- });
- }
- },
parseDailyUsageData: function(usageData) {
@@ -260,21 +261,6 @@ var dash = {
},
- pollMonthStats: function() {
- if(this.pollingEnabled) {
- $.ajax({
- url: "/energy-usage/1/month-stats",
- type: "GET",
- success: function(data) {
- dash.parseMonthlyUsageData(data);
- },
- dataType: "json",
- complete: setTimeout(function() {dash.pollMonthStats()}, dash.historicalStatsPollRateMs),
- timeout: 4000
- });
- }
- },
-
parseMonthlyUsageData: function(usageData) {
// Clear previous data
@@ -311,21 +297,6 @@ var dash = {
$("#avg-month").text(avg.toFixed(2));
},
- pollPowerStatus: function() {
- if(this.pollingEnabled) {
- $.ajax({
- url: "/power-state/1",
- type: "GET",
- success: function(data) {
- dash.refreshPowerState(data);
- },
- dataType: "json",
- complete: setTimeout(function() {dash.pollPowerStatus()}, dash.powerStatePollRateMs),
- timeout: 2000
- });
- }
- },
-
refreshPowerState: function(powerState) {
if(powerState.isOn) {
$("#power-state").text("ON").attr("class", "label label-success");
@@ -334,23 +305,13 @@ var dash = {
$("#power-state").text("OFF").attr("class", "label label-danger");
}
- $("#uptime").text(moment.duration(powerState.uptime, "seconds").format("d[d] h[h] m[m]"));
- },
-
-};
-
-
-$(document).ready(function () {
-
- dash.init();
-
- $("#toggle-polling").change(function() {
- if(this.checked) {
- dash.startPolling();
+ if(powerState.uptime === 0) {
+ $("#uptime").text("-");
}
else {
- dash.stopPolling();
+ $("#uptime").text(moment.duration(powerState.uptime, "seconds").format("d[d] h[h] m[m]", {largest: 2}));
}
- });
+
+ },
-});
+};
diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css
index 3f4c1ca..2057c4d 100644
--- a/public/stylesheets/style.css
+++ b/public/stylesheets/style.css
@@ -25,4 +25,65 @@ footer ul {
color: #fff;
background-color: #5cb85c;
border-color: #4cae4c;
-} \ No newline at end of file
+}
+
+.alert-danger {
+ color: #a94442;
+ background-color: #f2dede;
+ border-color: #ebccd1;
+}
+
+.list-inline > .active {
+ font-weight: bold;
+}
+
+.nav_title {
+ width: 170px;
+}
+
+.nav-md .container.body .col-md-3.left_col {
+ width: 170px;
+}
+
+.nav-md .container.body .right_col {
+ margin-left: 170px;
+}
+
+@media (min-width: 992px) {
+ footer {
+ margin-left: 170px;
+ }
+}
+
+@media (max-width: 991px) {
+ .nav-md .container.body .right_col {
+ padding-right: 0;
+ margin-left: 0;
+ }
+}
+
+@media (max-width: 1100px) {
+ .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-xs-12, .col-lg-2 {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+@media (min-width: 1635px) {
+ .col-xl-2,{
+ float: left;
+ }
+ .col-xl-2 {
+ width: 16.66666667%;
+ }
+}
+
+.device-list-small{
+ display: none;
+}
+
+@media (max-width: 991px) {
+ .device-list-small{
+ display: inline;
+ }
+}
diff --git a/routes/energy-usage.js b/routes/energy-usage.js
deleted file mode 100644
index eb21eb9..0000000
--- a/routes/energy-usage.js
+++ /dev/null
@@ -1,166 +0,0 @@
-const express = require('express');
-const router = express.Router();
-
-const deviceManager = require('../services/device-manager');
-const moment = require('moment');
-
-router.get('/:deviceId/realtime', function(req, res, next) {
-
- let deviceId = req.params.deviceId;
-
- let realtimeUsage = {};
- // TODO - cache results with a short TTL so we don't hammer the plug if multiple clients are requesting data
- deviceManager.getDevice(deviceId).emeter.getRealtime().then(response => {
-
- // Voltage seems to be reported as its peak to peak value, not RMS.
- // Show the RMS value since thats what would you expect to see.
- // i.e. 220v not 310v (in the U.K)
- response.voltage = response.voltage / Math.sqrt(2);
-
- res.json(response);
- });
-
-});
-
-router.get('/:deviceId/day-stats', function(req, res, next) {
-
- let deviceId = req.params.deviceId;
-
- // Get last x days
- let totalDaysRequired = 30; // TODO currently only works for up to 2 months spans
- let currentMoment = moment();
- let previousMoment = moment().subtract(totalDaysRequired, 'days');
-
- // Month + 1 as the API months are index 1 based.
- deviceManager.getDevice(deviceId).emeter.getDayStats(currentMoment.year(), currentMoment.month() +1).then(currentPeriodStats => {
-
- // Check if we also need the previous month to meet the required total number of samples
- if(currentMoment.month() !== previousMoment.month()) {
-
- // Get previous month. This currently wont work if the previousMoment is more than 1 month before the currentMoment (see above)
- deviceManager.getDevice(deviceId).emeter.getDayStats(previousMoment.year(), previousMoment.month() +1).then(previousPeriodStats => {
-
- let currentMonthStats = fillMissingDays(currentPeriodStats, currentMoment);
- let previousMonthStats = fillMissingDays(previousPeriodStats, previousMoment);
- let combinedStats = previousMonthStats.concat(currentMonthStats);
-
- res.json(trimStatResults(combinedStats, totalDaysRequired));
-
- });
- }
- else {
- let dayStats = fillMissingDays(currentPeriodStats, currentMoment);
-
- res.json(trimStatResults(dayStats, totalDaysRequired));
- }
-
- });
-
-});
-
-router.get('/:deviceId/month-stats', function(req, res, next) {
- let deviceId = req.params.deviceId;
-
- // Get last x months
- let totalMonthsRequired = 12; // TODO currently only works for up to 14 month (2 year) spans
- let currentMoment = moment();
- let previousMoment = moment().subtract(totalMonthsRequired, 'months');
-
- deviceManager.getDevice(deviceId).emeter.getMonthStats(currentMoment.year()).then(currentPeriodStats => {
-
- // Check if we also need the previous year to meet the required total number of samples
- if(currentMoment.month() + 1 < totalMonthsRequired) {
-
- // Get previous year (assuming the totalMonthsRequired limit described above).
- deviceManager.getDevice(deviceId).emeter.getMonthStats(previousMoment.year()).then(previousPeriodStats => {
-
- let currentYearStats = fillMissingMonths(currentPeriodStats, currentMoment);
- let previousYearStats = fillMissingMonths(previousPeriodStats, previousMoment);
- let combinedStats = previousYearStats.concat(currentYearStats);
-
- res.json(trimStatResults(combinedStats, totalMonthsRequired));
-
- });
- }
- else {
- let monthStats = fillMissingMonths(currentPeriodStats, currentMoment);
-
- res.json(trimStatResults(monthStats, totalMonthsRequired));
- }
-
- });
-
-});
-
-function fillMissingDays(sparseDayStats, statsMoment) {
- let denseDayStats = [];
-
- let totalDays;
- // If these stats are for the current month, fill up to the current day of the month
- // Otherwise fill the whole month
- if(moment().month() === statsMoment.month()) {
- totalDays = statsMoment.date();
- }
- else {
- totalDays = statsMoment.daysInMonth();
- }
-
- Array.from({length: totalDays}, (x,i) => i + 1).forEach(d => {
-
- let stat = sparseDayStats.day_list.find(i => i.day === d);
-
- if(stat === undefined) {
- denseDayStats.push({
- year: statsMoment.year(),
- month: statsMoment.month() +1,
- day: d,
- energy: 0
- })
- }
- else {
- denseDayStats.push(stat);
- }
-
- });
-
- return denseDayStats;
-}
-
-function fillMissingMonths(sparseMonthStats, statsMoment) {
- let denseMonthStats = [];
-
- let maxMonths;
- // Dont fill in months which exist in the future
- if(statsMoment.year() === moment().year()) {
- maxMonths = moment().month() + 1; // API months are 1 based
- }
- else {
- maxMonths = 12;
- }
-
- // Fill in any missing months up to the max amount
- Array.from({length: maxMonths}, (x,i) => i + 1).forEach(m => {
-
- let stat = sparseMonthStats.month_list.find(i => i.month === m);
-
- if(stat === undefined) {
- denseMonthStats.push({
- year: statsMoment.year(),
- month: m,
- energy: 0
- })
- }
- else {
- denseMonthStats.push(stat);
- }
-
- });
-
- return denseMonthStats;
-}
-
-function trimStatResults(stats, maxSamples) {
- return stats.splice(stats.length - maxSamples, stats.length);
-}
-
-module.exports = router; \ No newline at end of file
diff --git a/routes/index.js b/routes/index.js
index 4909ff8..12c62cd 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -5,11 +5,27 @@ const deviceManager = require('../services/device-manager');
router.get('/', function(req, res, next) {
+ let deviceId = sortDevices(deviceManager.getAllDevices())[0].deviceId;
+
+ res.redirect('/' + deviceId);
+
+});
+
+router.get('/:deviceId', function(req, res, next) {
+
+ let deviceId = req.params.deviceId;
+
res.render('index', {
- device: deviceManager.getDevice(),
- devices: deviceManager.getAllDevices()
+ device: deviceManager.getDevice(deviceId),
+ devices: sortDevices(deviceManager.getAllDevices())
});
});
+function sortDevices(devices) {
+ return devices.slice().sort((a, b) => {
+ return a.alias.toLowerCase().localeCompare(b.alias.toLowerCase())
+ })
+}
+
module.exports = router;
diff --git a/routes/power-state.js b/routes/power-state.js
deleted file mode 100644
index 6333863..0000000
--- a/routes/power-state.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const express = require('express');
-const router = express.Router();
-
-const deviceManager = require('../services/device-manager');
-const moment = require('moment');
-
-router.get('/:deviceId', function(req, res, next) {
-
- let deviceId = req.params.deviceId;
-
- deviceManager.getDevice(deviceId).getSysInfo().then(response => {
-
- let powerState = {
- isOn: (response.relay_state === 1),
- uptime: response.on_time
- };
-
- res.json(powerState);
- });
-
-});
-
-module.exports = router; \ No newline at end of file
diff --git a/routes/ws.js b/routes/ws.js
new file mode 100644
index 0000000..08b0a18
--- /dev/null
+++ b/routes/ws.js
@@ -0,0 +1,28 @@
+const express = require('express');
+const router = express.Router();
+
+const deviceManager = require('../services/device-manager');
+const dataFetcher = require('../services/data-fetcher');
+const dataBroadcaster = require('../services/data-broadcaster');
+
+router.ws('/', function(ws, req) {
+
+ ws.on('message', msg => {
+
+ let message = JSON.parse(msg);
+
+ // Latest data is always pushed out to clients, but clients can also request cached data at any time.
+ if(message.requestType === 'getCachedData') {
+ let deviceId = message.deviceId;
+ let cachedData = dataFetcher.getCachedData(deviceId);
+
+ ws.send(dataBroadcaster.generatePayload('realtimeUsage', deviceId, cachedData.realtimeUsage));
+ ws.send(dataBroadcaster.generatePayload('dailyUsage', deviceId, cachedData.dailyUsage));
+ ws.send(dataBroadcaster.generatePayload('monthlyUsage', deviceId, cachedData.monthlyUsage));
+ ws.send(dataBroadcaster.generatePayload('powerState', deviceId, cachedData.powerState));
+ }
+ });
+
+});
+
+module.exports = router;
diff --git a/services/data-broadcaster.js b/services/data-broadcaster.js
new file mode 100644
index 0000000..9e0c5d0
--- /dev/null
+++ b/services/data-broadcaster.js
@@ -0,0 +1,43 @@
+const app = require('../app');
+
+function broadcastRealtimeUsageUpdate(deviceId, data) {
+ broadcast(generatePayload('realtimeUsage', deviceId, data));
+}
+
+function broadcastDailyUsageUpdate(deviceId, data) {
+ broadcast(generatePayload('dailyUsage', deviceId, data));
+}
+
+function broadcastMonthlyUsageUpdate(deviceId, data) {
+ broadcast(generatePayload('monthlyUsage', deviceId, data));
+}
+
+function broadcastPowerStateUpdate(deviceId, data) {
+ broadcast(generatePayload('powersState', deviceId, data));
+}
+
+function broadcast(payload) {
+ app.getWsClients().forEach(client => {
+ client.send(payload);
+ })
+}
+
+function generatePayload(dataType, deviceId, data) {
+
+ let payload = {
+ dataType: dataType,
+ deviceId: deviceId,
+ data: data
+ }
+
+ return JSON.stringify(payload);
+}
+
+
+module.exports = {
+ broadcastRealtimeUsageUpdate: broadcastRealtimeUsageUpdate,
+ broadcastDailyUsageUpdate: broadcastDailyUsageUpdate,
+ broadcastMonthlyUsageUpdate: broadcastMonthlyUsageUpdate,
+ broadcastPowerStateUpdate: broadcastPowerStateUpdate,
+ generatePayload: generatePayload
+} \ No newline at end of file
diff --git a/services/data-fetcher.js b/services/data-fetcher.js
new file mode 100644
index 0000000..7ef3594
--- /dev/null
+++ b/services/data-fetcher.js
@@ -0,0 +1,277 @@
+const deviceManager = require('./device-manager');
+const dataBroadcaster = require('./data-broadcaster');
+const app = require('../app');
+const moment = require('moment');
+
+// Get initial data after a short delay to allow the device manager to find devices
+// TODO run once device manager notifies its complete instead
+setTimeout(function() {
+ fetchRealtimeUsage();
+ fetchDailyUsage();
+ fetchMonthlyUsage();
+ fetchPowerState();
+}, 2000)
+
+let cachedRealtimeUsageData = [];
+let cachedDailyUsageData = [];
+let cachedMonthlyUsageData = [];
+let cachedPowerState = [];
+
+function fetchRealtimeUsage() {
+
+ if(app.getWsClientCount() > 0 || cachedRealtimeUsageData.length === 0) {
+
+ deviceManager.getAllDevices().forEach(device => {
+
+ let deviceId = device.deviceId;
+ device.emeter.getRealtime().then(response => {
+
+ // Voltage seems to be reported as its peak to peak value, not RMS.
+ // Show the RMS value since thats what would you expect to see.
+ // i.e. 220v not 310v (in the U.K)
+ response.voltage = response.voltage / Math.sqrt(2);
+
+ updateCache(cachedRealtimeUsageData, deviceId, response);
+
+ dataBroadcaster.broadcastRealtimeUsageUpdate(deviceId, response);
+ });
+
+ });
+
+ }
+
+ setTimeout(fetchRealtimeUsage, 1000);
+}
+
+function fetchDailyUsage() {
+
+ if(app.getWsClientCount() > 0 || cachedDailyUsageData.length === 0) {
+
+ // Get last x days
+ let totalDaysRequired = 30; // TODO currently only works for up to 2 months spans
+ let currentMoment = moment();
+ let previousMoment = moment().subtract(totalDaysRequired, 'days');
+
+ // Month + 1 as the API months are index 1 based.
+ deviceManager.getAllDevices().forEach(device => {
+
+ let deviceId = device.deviceId;
+ device.emeter.getDayStats(currentMoment.year(), currentMoment.month() +1).then(currentPeriodStats => {
+
+ // Check if we also need the previous month to meet the required total number of samples
+ if(currentMoment.month() !== previousMoment.month()) {
+
+ // Get previous month. This currently wont work if the previousMoment is more than 1 month before the currentMoment (see above)
+ device.emeter.getDayStats(previousMoment.year(), previousMoment.month() +1).then(previousPeriodStats => {
+
+ let currentMonthStats = fillMissingDays(currentPeriodStats, currentMoment);
+ let previousMonthStats = fillMissingDays(previousPeriodStats, previousMoment);
+ let combinedStats = previousMonthStats.concat(currentMonthStats);
+
+ let result = trimStatResults(combinedStats, totalDaysRequired);
+
+ updateCache(cachedDailyUsageData, deviceId, result);
+
+ dataBroadcaster.broadcastDailyUsageUpdate(deviceId, result);
+
+ });
+ }
+ else {
+ let dayStats = fillMissingDays(currentPeriodStats, currentMoment);
+
+ let result = trimStatResults(dayStats, totalDaysRequired);
+ updateCache(cachedDailyUsageData, deviceId, result);
+
+ dataBroadcaster.broadcastDailyUsageUpdate(deviceId, result);
+ }
+
+ });
+
+ });
+
+ }
+
+ setTimeout(fetchDailyUsage, 300000); // 5 mins;
+}
+
+function fetchMonthlyUsage() {
+
+ if(app.getWsClientCount() > 0 || cachedMonthlyUsageData.length === 0) {
+
+ // Get last x months
+ let totalMonthsRequired = 12; // TODO currently only works for up to 14 month (2 year) spans
+ let currentMoment = moment();
+ let previousMoment = moment().subtract(totalMonthsRequired, 'months');
+
+ deviceManager.getAllDevices().forEach(device => {
+
+ let deviceId = device.deviceId;
+ device.emeter.getMonthStats(currentMoment.year()).then(currentPeriodStats => {
+
+ // Check if we also need the previous year to meet the required total number of samples
+ if(currentMoment.month() + 1 < totalMonthsRequired) {
+
+ // Get previous year (assuming the totalMonthsRequired limit described above).
+ device.emeter.getMonthStats(previousMoment.year()).then(previousPeriodStats => {
+
+ let currentYearStats = fillMissingMonths(currentPeriodStats, currentMoment);
+ let previousYearStats = fillMissingMonths(previousPeriodStats, previousMoment);
+ let combinedStats = previousYearStats.concat(currentYearStats);
+
+ let result = trimStatResults(combinedStats, totalMonthsRequired);
+
+ updateCache(cachedMonthlyUsageData, deviceId, result);
+
+ dataBroadcaster.broadcastMonthlyUsageUpdate(deviceId, result);
+
+ });
+ }
+ else {
+ let monthStats = fillMissingMonths(currentPeriodStats, currentMoment);
+
+ let result = trimStatResults(monthStats, totalMonthsRequired);
+
+ updateCache(cachedMonthlyUsageData, deviceId, result);
+
+ dataBroadcaster.broadcastMonthlyUsageUpdate(deviceId, result);
+ }
+
+ });
+
+ });
+
+ }
+
+ setTimeout(fetchMonthlyUsage, 1800000); // 30 mins
+}
+
+function fetchPowerState() {
+
+ if(app.getWsClientCount() > 0 || cachedPowerState.length === 0) {
+
+ deviceManager.getAllDevices().forEach(device => {
+
+ let deviceId = device.deviceId;
+ device.getSysInfo().then(response => {
+
+ let powerState = {
+ isOn: (response.relay_state === 1),
+ uptime: response.on_time
+ };
+
+ updateCache(cachedPowerState, deviceId, powerState);
+
+ dataBroadcaster.broadcastPowerStateUpdate(deviceId, powerState);
+ });
+ });
+
+ }
+ setTimeout(fetchPowerState, 60000);
+}
+
+
+function fillMissingDays(sparseDayStats, statsMoment) {
+ let denseDayStats = [];
+
+ let totalDays;
+ // If these stats are for the current month, fill up to the current day of the month
+ // Otherwise fill the whole month
+ if(moment().month() === statsMoment.month()) {
+ totalDays = statsMoment.date();
+ }
+ else {
+ totalDays = statsMoment.daysInMonth();
+ }
+
+ Array.from({length: totalDays}, (x,i) => i + 1).forEach(d => {
+
+ let stat = sparseDayStats.day_list.find(i => i.day === d);
+
+ if(stat === undefined) {
+ denseDayStats.push({
+ year: statsMoment.year(),
+ month: statsMoment.month() +1,
+ day: d,
+ energy: 0
+ })
+ }
+ else {
+ denseDayStats.push(stat);
+ }