Skip to content

Instantly share code, notes, and snippets.

@garoevans
Created September 25, 2018 07:21
Show Gist options
  • Save garoevans/e50ab743b611614133e84b5174afad17 to your computer and use it in GitHub Desktop.
Save garoevans/e50ab743b611614133e84b5174afad17 to your computer and use it in GitHub Desktop.
// 1) open the shared spreadsheed, make a copy of it, open tools|script editor, update the settings below, save
// https://docs.google.com/spreadsheets/d/15bTn9_Cv9IBVkvepINPlBRcK8oty74e6FlAieNbiQMg/edit?usp=sharing
// for script updates, simply copy this script into your spreadsheet using the script editor, i.e. select all, copy, paste, save
// 2) set the script's timezone (File|Project properties)
// 3) on the menu bar click Current Project's Triggers
// click add new trigger
// for Run select the function nestDataCollection, Events: time-driven, and select the rate per your preference (I do every 5 minutes)
// click add new trigger
// for Run select the function rollUpAllSheets, Events: time-driven, and select once a month
// 4) in the spreadsheet, set the spreadsheet's timezone to your timezone (File|Spreadsheet settings...)
// 5) duplicate the first tab for each additional nest thermostat managed by your account
// 6) name the tabs "{structure name} {thermostat name}", e.g. "Home Living Room", "Home Office", "Condo Master Bedroom", ...
//
// Based on
// bmw220 https://www.instructables.com/id/Nest-Thermostat-Data-Logger/
// michael-pesce https://gist.github.com/michael-pesce/a4ba55d4fc4f4df7d3a6
// beezly https://gist.github.com/beezly/9b2de3749d687fdbff3f
//
// user specific settings -- update the values for your nest account, or update the settings tab
var settings = {
EMAIL: 'EMAIL', // your nest user id / email
PASSWORD: 'PASSWORD', // your nest password
MINUTESPERSAMPLE: 15, // number of minutes between samples -- every 15 minutes seems reasonable, but adjust as you see fit
SAMPLESHIFT: 0, // number of minutes to shift the sample time -- use to delay the sample time by some minutes
COMPUTEUSAGE: true, // set to true if you want to compute usage on data collection
TEMPDECIMALS: 1, // number of decimals in final temperature results
TZDELTA: 0 // Difference in hours between the spread sheet's timezone and the first tab's thermostat timezone
};
// main entry point to collect data from nest and update the spreadsheet
// -- create a trigger to run this function as often as you need to collect the desired amount of data.
// -- It should be the same or faster than the minutes per sample.
// -- Triggering faster than MINUTESPERSAMPLE enables retry on nest login failure,
// -- e.g. Trigger every 5 minutes and MINUTESPERSAMPLE = 15 means try every 5 minutes, but collect data only every 15 minutes.
// -- I suggest something between 5 minutes to 1 hour.
function nestDataCollection() {
// skip if it is not time for a new sample
return nest.readyForNextSample() ? nest.doDataCollection() : 'skipped';
}
// main entry point to roll up thermostat data -- collapse all daily data to a single row for that day
// -- create a trigger to run this function once a month
function rollUpAllSheets() {
return nest.rollUpPenultimateMonth({sheets: SpreadsheetApp.getActiveSpreadsheet().getSheets()});
}
// add Nest menu to ui
// -- if you do not want to add this 'Nest' menu to the ui, then rename this function to something other than onOpen
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('Nest')
.addSubMenu(ui.createMenu('Roll Up')
.addItem('Roll Up Last Month', 'rollUpLastMonth')
.addItem('Roll Up Penultimate Month', 'rollUpPenultimateMonth')
.addItem('Roll Up Last Quarter', 'rollUpLastQuarter')
.addItem('Roll Up All', 'rollUpAll')
.addSeparator()
.addItem('Roll Up Until...', 'rollUpUntil')
.addItem('Roll Up...', 'rollUp'))
.addItem('Recompute usage', 'recomputeUsage')
.addItem('Collect data', 'nestDataCollection')
.addToUi();
return 'success';
}
// Google app script helpers
function rollUpLastMonth() { nest.rollUpLastMonth(); }
function rollUpPenultimateMonth() { nest.rollUpPenultimateMonth(); }
function rollUpLastQuarter() { nest.rollUpLastQuarter(); }
function rollUpAll() { nest.rollUpAll(); }
function rollUpUntil() { nest.rollUpUntil(); }
function rollUp() { nest.rollUp(); }
function recomputeUsage() { nest.recomputeUsage(); }
// get the Nest data collection class
var Nest = requireNest();
// instantiate a Nest data collection object
var nest = new Nest();
// return the nest data collection class
function requireNest() {
return (function () {
// Nest data collection class
function Nest() {
// compute settings
this.settings = this.extend({MAXCOLUMN : 13, CRYPTKEY: 13}, settings, this.getSheetSettings());
};
// roll up data
Nest.prototype.rollUp = function (options) {
return this.doRollUp(options);
}
// roll up without row delete -- useful to debug roll up
Nest.prototype.rollUpNoDelete = function (options) {
return this.doRollUp(this.extend({doNotDelete: true}, options));
}
// roll up all data
Nest.prototype.rollUpAll = function (options) {
return this.doRollUp(this.extend({startDate: '1/1/2000', endDate: '1/1/2099'}, options));
}
// roll up data until
Nest.prototype.rollUpUntil = function (options) {
return this.doRollUp(this.extend({startDate: '1/1/2000'}, options));
}
// roll up data last month
Nest.prototype.rollUpLastMonth = function (options) {
var month = (new Date()).getMonth();
return this.rollUpMonth(month > 0 ? month - 1 : 11, options);
}
// roll up data penultimate month
Nest.prototype.rollUpPenultimateMonth = function (options) {
var month = (new Date()).getMonth();
return nest.rollUpMonth(month > 1 ? month - 2 : month == 1 ? 11 : 10, options);
}
// roll up data month
Nest.prototype.rollUpMonth = function (month, options) {
var today = new Date();
return this.doRollUp(this.extend({startDate: new Date((month + 1) + '/1/' + today.getFullYear()), endDate: new Date((month < 11 ? (month + 2) : 1) + '/1/' + today.getFullYear())}, options));
}
// roll up data last quarter
Nest.prototype.rollUpLastQuarter = function (options) {
var today = new Date();
var endDate = undefined;
switch(today.getMonth()) {
case 0: case 1: case 2: default:
startDate = new Date('10/1/' + (today.getFullYear() - 1));
endDate = new Date('1/1/' + today.getFullYear());
break;
case 3: case 4: case 5:
startDate = new Date('1/1/' + today.getFullYear());
endDate = new Date('4/1/' + today.getFullYear());
break;
case 6: case 7: case 8:
startDate = new Date('4/1/' + today.getFullYear());
endDate = new Date('7/1/' + today.getFullYear());
break;
case 9: case 10: case 11:
startDate = new Date('7/1/' + today.getFullYear());
endDate = new Date('10/1/' + today.getFullYear());
break;
}
return this.doRollUp(this.extend({startDate: startDate, endDate: endDate}, options));
}
// recompute usage
Nest.prototype.recomputeUsage = function (options) {
return this.doRecomputeUsage(options);
}
// collect data from nest and update the spreadsheet
Nest.prototype.doDataCollection = function () {
Logger.log('Collect nest data...');
// login
this.login();
// collect nest information from the cloud
var nest = this.getSample();
Logger.log('Parse data...');
//Logger.log(JSON.stringify(nest));
// loop through structures
for (var sid in nest['structure']) {
var structure = nest['structure'][sid];
var devices = structure['devices'];
Logger.log('Parse structure: ' + (structure['name'] || sid) + '...');
// loop through devices
devices.forEach(function(did) {
var did = did.split('.')[1];
// current device information
var device = nest['device'][did];
var shared = nest['shared'][did];
// get thermostat name
var name = this.getThermostatName(nest, device, shared, sid);
Logger.log('Parse device: ' + (name || did) + '...');
// get the temperature scale for the thermostat, so we can access the appropriate temps
var tempUnits = device['temperature_scale'].toLowerCase();
// get the weather at the structure
var weather = this.getStructureWeather(nest, sid);
var gmtOffset = Math.round(parseFloat(weather['current']['gmt_offset']));
// build the row to append to the sheet
var newRow = [];
newRow[newRow.length] = this.getStructureTime(gmtOffset);
newRow[newRow.length] = name;
newRow[newRow.length] = shared['hvac_ac_state'] ? 'cool' : shared['hvac_heater_state'] ? 'heat' : structure['away'] ? 'away' : 'off';
newRow[newRow.length] = this.degree(shared['target_temperature'], tempUnits);
newRow[newRow.length] = this.degree(shared['current_temperature'], tempUnits);
newRow[newRow.length] = device['current_humidity'];
newRow[newRow.length] = weather['current']['temp_' + tempUnits];
newRow[newRow.length] = weather['current']['humidity'];
newRow[newRow.length] = weather['current']['condition'];
// get the sheet
var sheetName = structure['name'] + ' ' + name;
var sheet = this.getSheet(sheetName);
// write the new data to the sheet
Logger.log("Appending row to sheet: '" + sheetName + "'");
sheet.appendRow(newRow);
// compute usage
var lastRow = sheet.getLastRow();
if (this.settings.COMPUTEUSAGE && lastRow > 2) {
Logger.log("Compute usage...");
var row = lastRow - 1;
var usage = ((newRow[0] - sheet.getRange(row, 1).getValue()) / 1000 / 60 / 60).toFixed(2);
switch(sheet.getRange(row, 3).getValue()) {
case 'cool': sheet.getRange(row, 10).setValue(usage); break;
case 'heat': sheet.getRange(row, 11).setValue(usage); break;
case 'away': sheet.getRange(row, 12).setValue(usage); break;
}
}
}.bind(this));
}
// release data
this.auth = this.headers = this.zip = this.weather = undefined;
Logger.log("Success...");
return 'success';
}
// roll up data
Nest.prototype.doRollUp = function (options) {
// compute options
options = this.extend({startDate: null, endDate: null, uiEnabled: true, temperatureDecimals: this.settings.TEMPDECIMALS, doNotDelete: false, sheets: [SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()]}, options || {});
// if start and end dates are not given, ask the user
if ((!options.startDate || !options.endDate) && options.uiEnabled) {
// get the spreadsheet ui
var ui = SpreadsheetApp.getUi();
// if not start date
if (!options.startDate) {
// ask the user for the start roll up date
var response = ui.prompt('Nest data roll up', 'Enter a roll up start date (mm/dd/yyyy):', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() != ui.Button.OK)
return 'canceled';
options.startDate = response.getResponseText();
}
// ensure start date is mm/dd/yyyy
options.startDate = new Date(this.getRowDate(new Date(options.startDate)));
// if no end date
if (!options.endDate) {
// ask the user for the end roll up date
var response = ui.prompt('Nest data roll up', 'Enter a roll up end date (mm/dd/yyyy):', ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() != ui.Button.OK)
return 'canceled';
options.endDate = response.getResponseText();
}
// ensure end date is mm/dd/yyyy
options.endDate = new Date(this.getRowDate(new Date(options.endDate)));
// confirm the roll up dates with the user
response = ui.alert('Nest data roll up', 'Rolling up: ' + this.getRowDate(options.startDate) + ' to ' + this.getRowDate(options.endDate), ui.ButtonSet.OK_CANCEL);
if (response != ui.Button.OK || options.startDate >= options.endDate)
return 'canceled';
}
// ensure date options are dates with the time stripped -- this covers options passed in
options.startDate = new Date(this.getRowDate(new Date(options.startDate)));
options.endDate = new Date(this.getRowDate(new Date(options.endDate)));
Logger.log('rollUp ' + this.getRowDate(options.startDate) + ' to ' + this.getRowDate(options.endDate) + '...');
// for each sheet
options.sheets.forEach(function(sheet) {
Logger.log('Sheet ' + sheet.getName() + '...');
// set the current sheet
this.sheet = sheet;
// skip sheet if it is empty
if (this.lastRow() < 3)
return;
// finalize the accumlated row
function finalize() {
// if the values are within the date range
if (v0[0] < options.endDate) {
Logger.log('Finalize... ' + r0 + ', ' + n);
// if we accumulated values
if (n > 1) {
// finalize readings -- compute the averages
for (var c = 3; c < 8; c++) {
v0[c] = (v0[c] / n).toFixed(options.temperatureDecimals);
}
// finalize hvac state
if (!options.doNotDelete) {
switch((!this.isz(v0[9]) ? 1 : 0) + (!this.isz(v0[10]) ? 2 : 0) + (!this.isz(v0[11]) ? 4 : 0)) {
case 0: v0[2] = 'off'; break;
case 1: v0[2] = 'cool'; break;
case 2: v0[2] = 'heat'; break;
case 3: v0[2] = 'cool & heat'; break;
case 4: v0[2] = 'away'; break;
case 5: v0[2] = 'cool & away'; break;
case 6: v0[2] = 'heat & away'; break;
case 7: v0[2] = 'cool & heat & away'; break;
}
}
// record number of samples in the average
v0[12] = n;
// finalize weather
var mostFrequentWeather = Object.keys(weather)[0];
for (var w in weather) {
if (weather[w] > weather[mostFrequentWeather])
mostFrequentWeather = w;
}
v0[8] = mostFrequentWeather;
// delete the intermediate rows
if (!options.doNotDelete)
this.deleteRows(r0 + 1, n - 1);
}
else
// clear number of samples -- there is no average
v0[12] = '';
// finalize usage
v0[ 9] = this.isz(v0[ 9]) ? '' : v0[ 9].toFixed(2);
v0[10] = this.isz(v0[10]) ? '' : v0[10].toFixed(2);
v0[11] = this.isz(v0[11]) ? '' : v0[11].toFixed(2);
// write the finalized row
this.updateRow(r0, v0);
}
}
var r0 = 2; // row 0
var v0 = this.getRow(r0); // values for row 0
// find the starting row
while (v0[0] < options.startDate && r0 < this.lastRow()) {
v0 = this.getRow(++r0);
}
// initialize the accumulator
var t0 = this.getRowDate(v0[0]); // date for 0
var n = 1; // number of rows accumulated
var v1 = undefined; // values for row 1
var ti = v0[0]; // intermediate time used for accumulating usage
var si = v0[2]; // intermediate state used for accumulating usage
var weather = {}; // accumulate weather frequencies
// initialize usage computations
if (v0[0] < options.endDate) {
v0[9] = v0[10] = v0[11] = 0;
weather[v0[8].replace(/mostly |partly /i, '')] = 1;
}
// scan the sheet, accumulating rows as we go
for (var r1 = r0 + 1; r1 <= this.lastRow() && v0[0] < options.endDate; ) {
// get values for row 1
v1 = this.getRow(r1);
// accumulate hvac usage
var usage = (v1[0] - ti) / 1000 / 60 / 60;
if (/cool/i.test(si)) v0[ 9] += usage;
if (/heat/i.test(si)) v0[10] += usage;
if (/away/i.test(si)) v0[11] += usage;
// remember intermediate time and state
ti = v1[0];
si = v1[2];
// accumulate weather
var w = v1[8].replace(/mostly |partly /i, '');
weather[w] = weather[w] ? weather[w]++ : 1;
// get the date for row 1
var t1 = this.getRowDate(v1[0]);
// if rows are on the same day
if (t0 == t1) {
// accumulate data values
for (var c = 3; c < 8; c++) {
v0[c] += v1[c];
}
// clear intermediate usage numbers -- they are no longer needed
v1[9] = v1[10] = v1[11] = '';
// number of rows accumulated
n++;
// next row to accumulate
r1++;
} else {
// a new day
// finalize the current day
finalize.call(this);
// reinitialize accumulator for the new day
r0 = options.doNotDelete ? r1 : r0 + 1;
r1 = r0 + 1;
v0 = v1;
t0 = t1;
n = 1;
weather = {};
if (v0[0] < options.endDate) {
v0[9] = v0[10] = v0[11] = 0;
weather[v0[8].replace(/mostly |partly /i, '')] = 1;
}
}
}
// finalize the last row
finalize.call(this);
// update the sheet
this.updateSheet();
}.bind(this));
Logger.log("Success...");
return 'success';
}
// recompute usage
Nest.prototype.doRecomputeUsage = function (options) {
Logger.log('Recompute usage...');
// compute options
options = this.extend({sheets: [SpreadsheetApp.getActiveSpreadsheet().getActiveSheet()]}, options || {});
// for each sheet
options.sheets.forEach(function(sheet) {
Logger.log('Sheet ' + sheet.getName() + '...');
// set the current sheet
this.sheet = sheet;
// skip sheet if it is empty
if (this.lastRow() < 3)
return;
// for each row
var v0 = this.getRow(2);
for (var row = 3; row <= this.lastRow(); row++) {
var v1 = this.getRow(row);
// compute usage
var usage = ((v1[0] - v0[0]) / 1000 / 60 / 60).toFixed(2);
// update usage
switch(v0[2]) {
case 'cool': v0[ 9] = usage; v0[10] = v0[11] = ''; break;
case 'heat': v0[10] = usage; v0[ 9] = v0[11] = ''; break;
case 'away': v0[11] = usage; v0[ 9] = v0[10] = ''; break;
default: v0[ 9] = ''; v0[10] = v0[11] = ''; break;
}
// update the row
this.updateRow(row - 1, v0);
// next row
v0 = v1;
}
// clear the usage in the last row
v0[9] = v0[10] = v0[11] = '';
// update the row
this.updateRow(row - 1, v0);
// update the sheet
this.updateSheet();
}.bind(this));
Logger.log("Success...");
return 'success';
}
// login to nest
Nest.prototype.login = function (resolve) {
Logger.log('Logging in...');
// login options
var data = {
'email': this.settings.EMAIL,
'password': this.settings.PASSWORD
}
var options = {
'method' : 'post',
'muteHttpExceptions': true,
'headers': {
'Accept-Language': 'en-us',
'Accept': 'application/json',
'Connection': 'keep-alive'
},
'contentType': 'application/json',
'payload': JSON.stringify(data)
};
// log in to nest
// -- login is throttled by nest, my guess is that login fails when nest is 'busy' or otherwise determines too many logins have been made in some time period
var response = UrlFetchApp.fetch('https://home.nest.com/session', options);
if (response.getResponseCode() < 300) {
// successful login
// update settings
this.auth = JSON.parse(response);
this.headers = {
'Authorization': 'Basic ' + this.auth['access_token'],
'X-nl-user-id': this.auth['userid'],
'X-nl-protocol-version': '1',
'Accept-Language': 'en-us',
'Accept': 'application/json',
'Connection': 'keep-alive'
};
} else {
// login failed -- we will try again with the next trigger
var errorMessage = 'Login failed: ' + response.getContentText() + response.getAllHeaders().toSource();
Logger.log(errorMessage);
throw errorMessage;
}
}
// get Nest thermostat data sample
Nest.prototype.getSample = function () {
Logger.log('Getting nest data...');
return JSON.parse(UrlFetchApp.fetch(this.auth['urls']['transport_url'] + '/v2/mobile/user.' + this.auth['userid'], {headers: this.headers}));
}
// get weather at the structure
Nest.prototype.getStructureWeather = function (nest, sid) {
var zip = nest['structure'][sid]['postal_code'];
if (this.zip != zip || !this.weather) {
Logger.log('Getting weather...');
this.weather = JSON.parse(UrlFetchApp.fetch(this.auth['urls']['weather_url'] + zip, {headers: this.headers}))[zip.replace(/^0*/, '')];
if (this.weather)
this.zip = zip;
else {
this.weather= {current: {gmt_offset: "0", temp_f: 0, temp_c: 0, humidity: 0, condition: 'missing'}};
this.zip = undefined;
}
}
return this.weather;
}
// get the thermostat name
Nest.prototype.getThermostatName = function (nest, device, shared, sid) {
// use the device name given by the user
var name = shared['name'];
if (!name) {
// use the device location
// scan the locations
var wheres = nest['where'][sid]['wheres'];
for (var widx = 0; widx < wheres.length; widx++) {
// if the location is the device location
if (wheres[widx]['where_id'] == device['where_id']) {
// use the device location as the name
name = wheres[widx]['name'];
break;
}
}
}
return name;
}
// get the current time at the structure
Nest.prototype.getStructureTime = function (gmtOffsetInHours) {
var d = new Date();
return new Date(d.getTime() + 60000 * (d.getTimezoneOffset() + (gmtOffsetInHours * 60)));
}
// convert temp to fahrenheit or celcius
Nest.prototype.degree = function (temp, tempUnits) {
return tempUnits == 'f' ? Math.round(9.0 / 5.0 * temp + 32) : temp;
}
// get the row's date -- strip the time
Nest.prototype.getRowDate = function (t) {
return Utilities.formatDate(new Date(t), Session.getScriptTimeZone(), "MM/dd/YYYY");
}
// return true if we are ready for the next sample
Nest.prototype.readyForNextSample = function () {
// assume ready for the next sample
var readyForNextSample = true;
// commpute when the sample was scheduled to occur
var now = this.getStructureTime(this.settings.TZDELTA - (new Date()).getTimezoneOffset() / 60);
var minutesPerSample = this.settings.MINUTESPERSAMPLE;
var minutes = Math.floor((now.getHours() * 60 + now.getMinutes()) / minutesPerSample) * minutesPerSample + this.settings.SAMPLESHIFT;
var hours = Math.floor(minutes / 60);
var sampleTime = new Date(now.getYear(), now.getMonth(), now.getDate(), hours, minutes - hours * 60);
// get the first sheet
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];
// if we have a previous sample
var lastRow = sheet.getLastRow();
if (lastRow > 1) {
// get the last sample time
var lastSampleTime = new Date(sheet.getRange(lastRow, 1).getValue());
// we are ready for a new sample if the last sample time is less than the sample time
readyForNextSample = lastSampleTime < sampleTime;
}
// return if it is time for a new sample
return readyForNextSample;
}
// is number zero
Nest.prototype.isz = function (n) {
return Math.abs(n) < 0.01;
}
// extend implementation
// usage extend(dest, source1, source2, source3, ...)
Nest.prototype.extend = function (dest) {
// iterate over source arguments
Array.prototype.slice.call(arguments, 1).forEach(function(source) {
// if we have a source
if (source) {
// for each property in source
for (var prop in source) {
// if source property is iterable
if (source[prop].constructor === Object) {
// if dest property does not yet exist or exists but is iterable
if (!dest[prop] || dest[prop].constructor === Object) {
// create dest property if needed
dest[prop] = dest[prop] || {};
// extend dest with source
this.extend(dest[prop], source[prop]);
} else {
// just copy source to dest
dest[prop] = source[prop];
}
} else {
// just copy source to dest
dest[prop] = source[prop];
}
}
}
}.bind(this));
// return extended property
return dest;
}
// get settings from the settings sheet
Nest.prototype.getSheetSettings = function () {
// sheet settings
var sheetSettings = {settingRow: {}};
// get the settings sheet
var sheet = this.getSheet('settings');
if (sheet && sheet.getName().toLowerCase() == 'settings') {
sheet.getRange(1, 1, sheet.getLastRow(), 2).getValues().forEach(function(setting, index) {
// setting names are in the first column, values are in the second
var name = setting[0];
var value = setting[1];
// if we have a setting name
if (name) {
// update the setting
sheetSettings[name] = value;
sheetSettings['settingRow'][name] = index + 1;
}
});
}
// if the sheet has a password
if (sheetSettings['PASSWORD']) {
// get the password
var pw = sheetSettings['PASSWORD'];
// if we have a password
if (pw) {
// passwords that start with '#:' are encrypted
if (pw.substring(0, 2) == '#:') {
// decrypt the password
sheetSettings['PASSWORD'] = this.decryptString(pw.substring(2))
} else {
// encrypt the password
sheet.getRange(sheetSettings['settingRow']['PASSWORD'], 2).setValue('#:' + this.encryptString(pw));
}
}
}
// return the sheet settings
return sheetSettings;
}
// find sheet by name -- default to first sheet
Nest.prototype.getSheet = function (sheetName) {
// get the list of sheets
var sheets = SpreadsheetApp.getActiveSpreadsheet().getSheets();
// return the first sheet as a default
var sheet = sheets[0];
// match lowercased sheet name
sheetName = sheetName.toLowerCase();
// scan sheets
for (var sheetIdx = 0; sheetIdx < sheets.length; sheetIdx++) {
// if we found the sheet name
if (sheetName == sheets[sheetIdx].getName().toLowerCase()) {
// return the matched sheet name
sheet = sheets[sheetIdx];
break;
}
}
return sheet;
}
// get the row values
Nest.prototype.getRow = function (r) {
if (!this.rows && !this.settings.DISABLESHEETCACHE) {
// sheet operations such as getRange, and get/setValue are REALLY slow
// it is MUCH faster to read the entire sheet into memory
Logger.log('Getting all rows...');
this.rows = this.sheet.getRange(1, 1, this.sheet.getLastRow(), this.settings.MAXCOLUMN).getValues();
Logger.log('Rows: ' + this.rows.length + ', ' + this.rows[0].length);
}
// return the row values
return this.rows ? this.rows[r - 1] : this.sheet.getRange(r, 1, 1, this.settings.MAXCOLUMN).getValues()[0];
}
// update row
Nest.prototype.updateRow = function (r, v) {
if (this.rows)
// update the row with a copy of V
this.rows[r - 1] = v.slice(0);
else
this.sheet.getRange(r, 1, 1, v.length).setValues([v]);
}
// delete rows
Nest.prototype.deleteRows = function (r, n) {
if (this.rows)
this.rows.splice(r - 1, n);
else
this.sheet.deleteRows(r, n);
}
// get last row index
Nest.prototype.lastRow = function () {
return this.rows ? this.rows.length : this.sheet.getLastRow();
}
// update sheet
Nest.prototype.updateSheet = function () {
// update the sheet en masse
if (this.rows) {
var lastRow = this.sheet.getLastRow();
Logger.log('Setting all rows... ' + this.rows.length + ', ' + this.rows[0].length + ', ' + lastRow);
// update the values
this.sheet.getRange(1, 1, this.rows.length, this.rows[0].length).setValues(this.rows);
// delete extra rows
if (this.rows.length < lastRow)
this.sheet.deleteRows(this.rows.length + 1, lastRow - this.rows.length);
// release data
this.rows = undefined;
}
// release the sheet
this.sheet = undefined;
}
// encrypt a string
Nest.prototype.encryptString = function (s) {
return Utilities.base64Encode(this.xorEncode(s));
}
// decrypt a string
Nest.prototype.decryptString = function (s) {
return this.xorEncode(String.fromCharCode.apply(undefined, Utilities.base64Decode(s)));
}
Nest.prototype.xorEncode = function (s) {
// optionaly provide key for XOR obfuscation -- the key must not occur in s, otherwise an intermediate null is generated
// -- use settings because this.settings is not yet available
var key = settings.CRYPTKEY ? settings.CRYPTKEY : 13;
// convert string to array of characters, then numbers, then xor encode
s = s.split('')
.map(function(c){return c.charCodeAt(0);})
.map(function(i){return i ^ key;})
// return encoded string
return String.fromCharCode.apply(undefined, s);
};
// return the Nest data collection class
return Nest;
})();
}
@sheetd
Copy link

sheetd commented Mar 2, 2019

This is great. Thanks for this. One question, what are the units for the "Heater Usage" column? Not really sure what this number represents.

@ThinkMaize
Copy link

Is it possible to use this after migrating my Nest account to "Sign in with Google"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment