UNPKG

hyperbutter-nest

Version:

A plugin to monitor and control your Nest thermostat in the Hyper Butter server

312 lines (253 loc) 8.02 kB
'use strict'; const EventEmitter = require('events'); const util = require('util'); const api = require('unofficial-nest-api'); function Nest(config, settings) { EventEmitter.call(this); let defaultNest; let subscribeTimer; let updateTimer; const history = { days: undefined, data: {}, }; // setup listeners this.init = () => { this.emit('subscribe', { 'set-temperature': this.setTemperature, 'set-away': this.setAway, 'get-update': this.update, }); }; // setup the API if (!config.username || !config.password) { this.emit('error', 'Config missing "username" and/or "password"'); } else { api.login(config.username, config.password, (error, data) => { if (error) { this.emit('error', error.message); return; } this.emit('status', 'Connected to API'); this.emit('connected'); api.fetchStatus((data) => { // build out the Protects let protectIds = []; for (let id in data.topaz) { protectIds.push(id); } // show the available smoke alarms if (protectIds.length > 0) { this.emit('status', 'Available smoke alarms:', protectIds); } this.emit('status', 'Available thermostats:', api.getDeviceIds()); // set a default device id defaultNest = config.deviceId || api.getDeviceIds()[0]; // send out an update this.update(); //var ids = api.getDeviceIds(); //api.setTemperature(ids[0], 70); //api.setTemperature(70); //api.setFanModeAuto(); startSubscribe(); //api.setAway(); //api.setHome(); //api.setTargetTemperatureType(ids[0], 'heat'); }); }); } const startSubscribe = () => { stopSubscribe(); api.subscribe(handleSubscribe, ['shared', 'energy_latest', 'device', 'structure']); }; const stopSubscribe = () => { clearTimeout(subscribeTimer); subscribeTimer = undefined; }; const handleSubscribe = (deviceId, data, type) => { // data if set, is also stored here: api.lastStatus.shared[thermostatID] if (deviceId && deviceId === defaultNest) { this.emit('debug', `Device subscription update: ${deviceId} -> ${type}`); if (type === 'energy_latest') { // history of useage history.days = data.days; parseHistory(data); } else { // new data, go update this.update(); } } subscribeTimer = setTimeout(startSubscribe, config.subscribeMs || 2000); }; this.update = (callback) => { if (!api.lastStatus) { // try again real fast clearTimeout(updateTimer); updateTimer = setTimeout(this.update, 300, callback); return; } // clear it out clearTimeout(updateTimer); updateTimer = undefined; try { const status = api.lastStatus; const device = status.device[defaultNest]; const shared = status.shared[defaultNest]; const structure = status.structure[status.link[defaultNest].structure.split('.')[1]]; const data = { all: status }; // create something a little more readable data.current = { mode: device.current_schedule_mode.toLowerCase(), temperature: shared.current_temperature, humidity: device.current_humidity, ac: shared.hvac_ac_state, heat: shared.hvac_heater_state, altHeat: shared.hvac_alt_heat_state, fan: shared.hvac_fan_state, autoAway: shared.auto_away === 1, manualAway: structure.away, leaf: device.leaf, }; data.target = { mode: shared.target_temperature_type.toLowerCase(), temperature: shared.target_temperature, timeToTarget: device.time_to_target }; data.info = { name: shared.name, temperatureScale: device.temperature_scale, leafThresholdHeat: device.leaf_threshold_heat }; // set the history data.history = getHistory(); if (callback) callback(null, data); this.emit('update', data); } catch (error) { if (callback) callback(error); this.emit('error', 'Update error', error); } }; this.setTemperature = (temp, callback) => { if (temp === undefined) { const errorMsg = 'calling setTemperature without a temp is wrong (should be the .data param in your object)!'; this.emit('error', errorMsg); if (callback) callback(errorMsg); return; } api.setTemperature(defaultNest, Number(temp)); this.emit('status', `Setting temperature to: ${temp}`); if (callback) callback(null, temp); } this.setAway = (isAway, callback) => { if (isAway) api.setAway(); else api.setHome(); this.emit('status', `Setting away to: ${isAway}`); if (callback) callback(null, isAway); } const parseHistory = (data) => { const total = data.days.length; let chunks; let obj; // loop through the days add update the history object for(let i = 0; i < total; i++) { obj = data.days[i]; // group things by year-month so we can put it in the logs chunks = obj.day.split('-'); const historyDate = `${chunks[0]}-${chunks[1]}`; if (!history.data[historyDate]) { // new history.data[historyDate] = {}; } // set the history object history.data[historyDate][obj.day] = obj; } // open the log (or create it) for (let date in history.data) { writeLog(date, history.data[date]); } }; const writeLog = (name, days) => { if (config.skipLogging) { // send it off to the client this.update(); return; } // smoosh it together with the log directory const filename = `nest-${name}.log`; this.emit('read-log', filename, (error, data) => { if (error) { // we need to create the file this.emit('write-log', filename, JSON.stringify(days), (error) => { if (error) { this.emit('warn', `Not able to write log to: ${filename}`); return; } // send off the update this.update(); }); return; } // compare what we have with what is there const log = JSON.parse(data.toString()); let updateLog = false; for (let param in log) { if (!history.data[name][param]) { history.data[name][param] = log[param]; } } for (let param in history.data[name]) { if (!log[param]) { // we need to update the log updateLog = true; } } if (updateLog) { this.emit('write-log', filename, JSON.stringify(history.data[name]), (error) => { if (error) this.emit('warn', `Not able to write log to: ${filename}`); }); } this.update(); }); }; const getHistory = () => { // returns an object from the history object that the client can consume const dates = []; const obj = {}; let heat = 0; let avg = 0; let leafs = 0; let total = 0; for (let date in history) { dates.push(date); } dates.sort(); dates.reverse(); // latest month first let month = history.data[dates[0]]; for (let day in month) { heat += Number(month[day].total_heating_time); avg += Number(month[day].usage_over_avg); leafs += Number(month[day].leafs); total++; } obj.totalHeat = heat; obj.totalAvg = avg; obj.totalLeafs = leafs; obj.dayAvg = heat / total; // if we have a previous month, let's also send the leaf count along if (dates.length > 1) { month = history.data[dates[1]]; leafs = 0; for (let day in month) { leafs += Number(month[day].leafs); } obj.prevMonth = { totalLeafs: leafs }; } // set the days obj.days = history.days; return obj; }; return this; }; util.inherits(Nest, EventEmitter); module.exports = Nest;