Skip to content

Instantly share code, notes, and snippets.

@jamesbulpin
Created September 8, 2016 18:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamesbulpin/18f6c1da43c32f461238fa0c2afe6466 to your computer and use it in GitHub Desktop.
Save jamesbulpin/18f6c1da43c32f461238fa0c2afe6466 to your computer and use it in GitHub Desktop.
Process events from a Google calendar to create MQTT messages for lighting control etc.
var fs = require('fs');
var readline = require('readline');
var google = require('googleapis');
var googleAuth = require('google-auth-library');
var S = require('string');
var schedule = require('node-schedule');
var mqtt = require('mqtt');
// Based on sample code from https://developers.google.com/google-apps/calendar/quickstart/nodejs
var SCOPES = ['https://www.googleapis.com/auth/calendar'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'calendar-nodejs-quickstart.json';
var SEQUENCES = [["sunset", ["Bedroom1/Desk on", "Bedroom2/Lamp on"]],
["sunsetend", ["Bedroom1/Desk off", "Bedroom2/Lamp off"]]];
var j = schedule.scheduleJob(new Date((new Date()).getTime()+3600000), function() {
console.log("[" + new Date() + "] " + "Exiting after pre-set delay.");
process.exit(0);
});
var client = mqtt.connect('mqtt://localhost', {protocolId: 'MQIsdp', protocolVersion: 3});
client.on('connect', function () {
console.log("Connected to MQTT");
});
function runCommands(cmds, eventid, calendar, calmyid, auth) {
//console.log(":: %s,%s,%s,%s,%s", cmds, eventid, calendar, calmyid, auth);
cmds.forEach(function(item) {
console.log("[" + new Date() + "] " + "Executing: %s", item);
var parts = S(item).splitLeft(" ");
if (parts.length == 1) {
var p2 = S(parts[0]).splitLeft("=");
if (p2.length == 2) {
client.publish("KVP/" + p2[0], p2[1]);
}
}
else if (parts.length == 2) {
client.publish("Light/" + parts[0], parts[1]);
}
else if (parts.length == 3) {
client.publish("Light/" + parts[0] + "/" + parts[1], parts[2]);
}
console.log("[" + new Date() + "] " + "Actions complete for %s", item);
});
calendar.events.get({
auth: auth,
calendarId: calmyid,
eventId: eventid
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
//console.log(response);
response.location = "executed";
calendar.events.update({
auth: auth,
calendarId: calmyid,
eventId: eventid,
resource: response
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
});
});
}
// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
if (err) {
console.log('Error loading client secret file: ' + err);
return;
}
// Authorize a client with the loaded credentials, then call the
// Google Calendar API.
authorize(JSON.parse(content), listEvents);
});
/**
* Create an OAuth2 client with the given credentials, and then execute the
* given callback function.
*
* @param {Object} credentials The authorization client credentials.
* @param {function} callback The callback to call with the authorized client.
*/
function authorize(credentials, callback) {
var clientSecret = credentials.installed.client_secret;
var clientId = credentials.installed.client_id;
var redirectUrl = credentials.installed.redirect_uris[0];
var auth = new googleAuth();
var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
// Check if we have previously stored a token.
fs.readFile(TOKEN_PATH, function(err, token) {
if (err) {
getNewToken(oauth2Client, callback);
} else {
oauth2Client.credentials = JSON.parse(token);
callback(oauth2Client);
}
});
}
/**
* Get and store new token after prompting for user authorization, and then
* execute the given callback with the authorized OAuth2 client.
*
* @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
* @param {getEventsCallback} callback The callback to call with the authorized
* client.
*/
function getNewToken(oauth2Client, callback) {
var authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question('Enter the code from that page here: ', function(code) {
rl.close();
oauth2Client.getToken(code, function(err, token) {
if (err) {
console.log('Error while trying to retrieve access token', err);
return;
}
oauth2Client.credentials = token;
storeToken(token);
callback(oauth2Client);
});
});
}
/**
* Store token to disk be used in later program executions.
*
* @param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token));
console.log('Token stored to ' + TOKEN_PATH);
}
/**
* Lists the next 10 events on the user's primary calendar.
*
* @param {google.auth.OAuth2} auth An authorized OAuth2 client.
*/
function listEvents(auth) {
var calendar = google.calendar('v3');
var calmyid = null;
calendar.calendarList.list({
auth: auth
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var cals = response.items;
if (cals.length == 0) {
console.log('No calendars found.');
} else {
//console.log('Calendars:');
for (var i = 0; i < cals.length; i++) {
var cal = cals[i];
var calid = cal.id;
var calsumm = cal.summary;
//var caldesc = cal.description;
//console.log("%s/%s/%s", calid, calsumm, caldesc);
if (calsumm == "HomeAutomation") {
calmyid = calid;
}
}
}
if (calmyid == null) {
console.log('Could not find "HomeAutomation" calendar');
return;
}
calendar.events.list({
auth: auth,
calendarId: calmyid,
timeMin: (new Date((new Date()).getTime()-1*3600000)).toISOString(),
timeMax: (new Date((new Date()).getTime()+1*86400000)).toISOString(),
singleEvents: true,
orderBy: 'startTime'
}, function(err, response) {
if (err) {
console.log('The API returned an error: ' + err);
return;
}
var events = response.items;
if (events.length == 0) {
console.log('No upcoming events found.');
} else {
// Store an array of defined periods (i.e. holidays)
var periods = [];
for (var i = 0; i < events.length; i++) {
var event = events[i];
var eid = event.summary;
if ((eid.substr(0,1) == '#') || (eid.substr(0,5) == 'TEST#')) {
var tag = null;
if (eid.substr(0,1) == '#') {
tag = eid.substr(1);
}
else {
tag = eid.substr(5);
}
var start = event.start.dateTime || event.start.date;
var end = event.end.dateTime || event.end.date;
//console.log("Period defined: '%s' %s-%s", tag, start, end);
periods.push([tag, start, end]);
}
}
for (var i = 0; i < events.length; i++) {
var event = events[i];
var start = event.start.dateTime || event.start.date;
var end = event.end.dateTime || event.end.date;
var eid = event.summary;
if ((eid.substr(0,1) == '#') || (eid.substr(0,5) == 'TEST#')) {
// This is a defined period, skip here
}
else if (event.location == "executed") {
console.log("Skipping %s at %s, already run", eid, start);
// Already run this, skip it
}
else {
var isenabled = true;
S(eid).splitLeft(" ").forEach(function(crit) {
if ((crit.substr(0, 1) == "@") || (crit.substr(0, 1) == "!")) {
var enabled = null;
var disabled = null;
periods.forEach(function(item) {
var ptag = item[0];
var pstart = item[1];
var pend = item[2];
//console.log("Checking %s %s %s", ptag, pstart, pend);
if (crit.substr(1) == ptag) {
if ((start >= pstart) && (start <= pend)) {
// Event starts within period
if (crit.substr(0, 1) == '@') {
if (enabled) {
enabled = enabled + " " + ptag
}
else {
enabled = ptag
}
}
if (crit.substr(0, 1) == '!') {
if (disabled) {
disabled = disabled + " " + ptag
}
else {
disabled = ptag
}
}
}
//else {
// // Event starts outside period
// if (crit.substr(0, 1) == '!') {
// if (enabled) {
// enabled = enabled + " " + ptag
// }
// else {
// enabled = ptag
// }
// }
//if (crit.substr(0, 1) == '@') {
// if (disabled) {
// disabled = disabled + " " + ptag
// }
// else {
// disabled = ptag
// }
//}
//}
}
});
// Summarise the enable/disable - this is necessary
// because we only want to disable if no instance of the
// matching period was hit (for @)
if (crit.substr(0, 1) == '@') {
// No matching periods were found
if (!enabled) {
isenabled = false;
}
}
if (crit.substr(0, 1) == '!') {
// At least one matching period was found
if (disabled) {
isenabled = false;
}
}
};
});
if (isenabled) {
var cmd = S(event.summary).splitLeft(" ").filter(function(a){if ((a.substr(0,1) != '@')&&(a.substr(0,1) != '!')) return true}).join(" ")
console.log('lightwaverf %s %s', start, cmd);
//commandQueue.push([start, cmd]);
var j = schedule.scheduleJob(start, function(cmd, eventid, calendar, calmyid, auth){
console.log("Executing scheduled command: %s", cmd);
if (cmd.substr(0, 9) == "sequence ") {
var seqname = cmd.substr(9);
var cmds = [];
SEQUENCES.forEach(function(item) {
if (item[0] == seqname) {
cmds = item[1];
}
});
runCommands(cmds, eventid, calendar, calmyid, auth);
}
else {
runCommands([cmd], eventid, calendar, calmyid, auth);
}
}.bind(null, cmd, event.id, calendar, calmyid, auth));
}
//if (event.id == "n1ibr0rsuqujbdt9pcmr71pbe8_20151007T072000Z") {
// event.location = "actioned";
// calendar.events.update({
// auth: auth,
// calendarId: calmyid,
// eventId: event.id,
// resource: event
// }, function(err, response) {
// if (err) {
// console.log('The API returned an error: ' + err);
// return;
// }
// });
//}
}
}
}
});
});
}
@jamesbulpin
Copy link
Author

Don't beat me up about the messy, hacky code or the hardcoded stuff. Life's too short :)

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