aboutsummaryrefslogtreecommitdiff
path: root/services
diff options
context:
space:
mode:
Diffstat (limited to 'services')
-rw-r--r--services/data-broadcaster.js43
-rw-r--r--services/data-fetcher.js277
-rw-r--r--services/device-manager.js12
3 files changed, 328 insertions, 4 deletions
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);
+ }
+
+ });
+
+ 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);
+}
+
+function getCachedData(cache, deviceId) {
+ let cacheEntry = cache.find(d => d.deviceId == deviceId);
+ if(cacheEntry === undefined) {
+ return cacheEntry;
+ }
+ else {
+ return cacheEntry.data;
+ }
+}
+
+function updateCache(cache, deviceId, data) {
+
+ let cachedData = cache.find(d => d.deviceId == deviceId);
+
+ if(cachedData === undefined) {
+ cache.push({
+ deviceId: deviceId,
+ data: data
+ });
+ }
+ else {
+ cachedData.data = data;
+ }
+}
+
+module.exports.getCachedData = function(deviceId) {
+
+ return {
+ realtimeUsage: getCachedData(cachedRealtimeUsageData, deviceId),
+ dailyUsage: getCachedData(cachedDailyUsageData, deviceId),
+ monthlyUsage: getCachedData(cachedMonthlyUsageData, deviceId),
+ powerState: getCachedData(cachedPowerState, deviceId)
+ }
+} \ No newline at end of file
diff --git a/services/device-manager.js b/services/device-manager.js
index 3ed4a26..e4d18f3 100644
--- a/services/device-manager.js
+++ b/services/device-manager.js
@@ -3,14 +3,18 @@ const { Client } = require('tplink-smarthome-api');
const client = new Client();
var devices = [];
-client.startDiscovery({deviceTypes: ['plug']}).on('plug-new', plug => {
+client.startDiscovery({
+ deviceTypes: ['plug'],
+ discoveryTimeout: 20000
+ }).on('plug-new', plug => {
console.log('Found device: ' + plug.alias + ' [' + plug.deviceId + ']');
devices.push(plug);
-})
+});
module.exports.getDevice = function(deviceId) {
- // TODO - get by id
- return devices[0];
+
+ return devices.find(d => d.deviceId == deviceId);
+
}
module.exports.getAllDevices = function() {