hyperbutter-nest
Version:
A plugin to monitor and control your Nest thermostat in the Hyper Butter server
312 lines (253 loc) • 8.02 kB
JavaScript
;
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;