Skip to content

Commit

Permalink
Support for automation rules, scheduled tasks, scenes, and devices
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonywebb committed Nov 9, 2014
1 parent 3ff47a8 commit 2c58370
Show file tree
Hide file tree
Showing 6 changed files with 1,480 additions and 42 deletions.
82 changes: 64 additions & 18 deletions cgate.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
var net = require('net');
var carrier = require('carrier');
var common = require('./common');
var config = require('./config');

// TELNET SESSION TO CONTROL
var control = net.createConnection(config.cgate.contolport,config.cgate.host);
var control = {};
var events = {};
var statuses = {};

// TELNET CHANNEL TO STATUS UPDATES
var events = net.createConnection(config.cgate.eventport,config.cgate.host);

// TELNET CHANNEL TO STATUS UPDATES
var statuses = net.createConnection(config.cgate.statusport,config.cgate.host);

exports.init = function(io){
exports.init = function(){
// TELNET SESSION TO CONTROL
control = net.createConnection(CONFIG.cgate.contolport,CONFIG.cgate.host);
carrier.carry(control, function(line) {
pushRealtime('controlStream',line);
});

// TELNET CHANNEL TO STATUS UPDATES
events = net.createConnection(CONFIG.cgate.eventport,CONFIG.cgate.host);
carrier.carry(events, function(line) {
pushRealtime('eventStream',line);
});

// TELNET CHANNEL TO STATUS UPDATES
statuses = net.createConnection(CONFIG.cgate.statusport,CONFIG.cgate.host);
carrier.carry(statuses, function(line) {
pushRealtime('statusStream',line);
});
Expand All @@ -29,19 +28,59 @@ exports.init = function(io){
function pushRealtime(type, message) {
console.log(type+' : '+message);
// every message, before being sent out needs to be parsed to create a nice object that can be consumed
var parsedMessage = parseMessage(message);
io.emit(type, parsedMessage);
var parsedMessage = parseMessage(message,type);
IO.emit(type, parsedMessage);
}

return module.exports;
}

exports.write = function(msg){
control.write(msg);
if(msg){
control.write(msg);
}
}

exports.cmdString = function(device,command,level,delay) {
var message = '';

if(command=='on') {
message = 'ON //'+CONFIG.cgate.cbusname+'/'+CONFIG.cgate.network+'/'+CONFIG.cgate.application+'/'+device+'\n';
}
else if (command=='off') {
message = 'OFF //'+CONFIG.cgate.cbusname+'/'+CONFIG.cgate.network+'/'+CONFIG.cgate.application+'/'+device+'\n';
}
else if (command=='ramp') {

if (level <= 100) {
if (delay) {
message = 'RAMP //'+CONFIG.cgate.cbusname+'/'+CONFIG.cgate.network+'/'+CONFIG.cgate.application+'/'+device+' '+level+'% '+delay+'\n';
} else {
message = 'RAMP //'+CONFIG.cgate.cbusname+'/'+CONFIG.cgate.network+'/'+CONFIG.cgate.application+'/'+device+' '+level+'%\n';
}
}
}
return message;
}

function humanLevelValue(level) {
// convert levels from 0-255 to 0-100
var temp = Math.round((level/255)*100)

if(temp > 100){
temp = 100;
}
else if(temp < 0){
temp = 0;
}

return temp;
}

////////////////////////
// MESSAGE PROCESSING
////////////////////////
function parseMessage(data) {
function parseMessage(data,type) {
console.log(data);

var packet = {raw:data};
Expand All @@ -64,10 +103,14 @@ function parseMessage(data) {
var parseoid = array[4];

if (packet.action == 'ramp') {
packet.level = array[3];
packet.level = humanLevelValue(array[3]);
packet.time = array[4];
parseunit = array[5];
parseoid = array[6];
} else if (packet.action == 'on') {
packet.level = 100;
} else if (packet.action == 'off') {
packet.level = 0;
}

temp = parseunit.split('=');
Expand All @@ -91,8 +134,11 @@ function parseMessage(data) {

console.log(packet);

// are there custom things we want to do when this event occurs?
common.processMessage(packet);
// are there custom things we want to do when this event occurs? ONLY do this for the status stream
if(type=='statusStream'){
COMMON.processMessage(packet);
}


return packet;
}
198 changes: 196 additions & 2 deletions common.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,197 @@
exports.processMessage = function(msg){
// process rules that are contingent on msg events
var moment = require('moment');
var _ = require('underscore');
var tm = require('time');
var suncalc = require('suncalc');
var request = require('request');

var timers = {};


////////////////////////////
// MESSAGE/RULES PROCESSING
////////////////////////////

exports.processMessage = function(packet){
// process rules that are contingent on msg events and update the DB model

// just ignore packets of devices that are not in our DB
if(DB.devices[packet.group]){
var oldLevel = DB.devices[packet.group].level;

// record the timestamp of this level change and update the current level
DB.devices[packet.group].lastchange = moment().unix();
DB.devices[packet.group].level = packet.level;

processRules(packet.group,DB.devices[packet.group]);

// TODO: not sure how to accomplish this without knowing that the status event was driven by a button press (is sourceunit == 1 what I am after?)
// kill any timers that might be running for this group if we got a manual state change
//if (packet.level > 0 && packet.sourceunit > 1) {
// killTimer(packet.group);
//}
}
}

exports.doCommands = function(cmdArray,level) {
processCommands(cmdArray,level);
}

function processCommands(cmdArray,level) {
// how many commands are we looping over?
var i = 0;

// define the delayed loop function
function delayedLoop() {

var waitTillNextCommand = 0;
var command = cmdArray[i];

if(command.type=='lighting') {
processLightingCommand(command,level);
}
else if(command.type == 'delay') {
waitTillNextCommand = command.delay;
}
else if(command.type == 'url') {
processUrlCommand(command);
}
else if(command.type == 'email') {
// TODO: email someone
}
else if(command.type == 'sms') {
// TODO: send sms
}
else if(command.type == 'rule') {
// TODO: enable/disable a rule
}
else if(command.type == 'task') {
// TODO: enable/disable a task
}
else if(command.type == 'scene') {
// TODO: trigger a scene
}
else {
console.log('Did not recognize command:');
console.log(command);
}

// if the end of the array has been reached, stop
if(++i == cmdArray.length) {
return;
}

// recursively call the delayed loop function with a delay
setTimeout(delayedLoop, 1000 * waitTillNextCommand);
}
// kick off these commands one at a time
delayedLoop();
}

function processRules(id,device) {
// first off we need to extract some values so we can process the expression
var dd = new tm.Date();
dd.setTimezone(CONFIG.location.timezone);

var times = suncalc.getTimes(dd,CONFIG.location.latitude,CONFIG.location.longitude);

// variables we can use in our expressions
var time = getTimeString(dd,dd.getTimezoneOffset()); // the current time
var sunset = getTimeString(times.sunset,dd.getTimezoneOffset()); // sun starts to set
var dusk = getTimeString(times.dusk,dd.getTimezoneOffset()); // sun has fully set and it is starting to get dark
var sunrise = getTimeString(times.sunrise,dd.getTimezoneOffset()); // sun has started to rise
var dawn = getTimeString(times.dawn,dd.getTimezoneOffset()); // sun has not risen but it is starting to get light
var group = id; // group address of the group that triggered the rules engine
var level = device.level; // this is the level of the group that triggered the rules engine, the rules say this is 0-100

console.log('---------------START PROCESSING RULES---------------');
console.log('rule vars: ' + time + ' ' + sunset + ' ' + dusk + ' ' + dawn + ' ' + sunrise + ' ' + group + ' ' + level);

_.each(DB.rules,function(rule,ind){
// only process enabled rules
if(rule.enabled){
if(eval(rule.expression)) {
console.log('----------- BINGO --------------')
console.log(rule);
var therules = rule.commands;
processCommands(therules,level);
}
}
if(DB.rules.length==ind+1){
console.log('---------------DONE PROCESSING RULES---------------');
}
})
}

function processUrlCommand(command) {
request(command.url, function (error, response, body) {
if (error && response.statusCode != 200) {
console.log('got back an error when calling '+command.url+': ('+response.statusCode+') '+err.message); // Print the google web page.
}
});
}

function processLightingCommand(command,level) {
// only need to adjust the actual lighting level (which we will cast to 0-100) if it is not at the level the scene says it needs to be
var changeto = command.level;
var cmdtext = 'ramp';

if(command.level!=DB.devices[command.group].level){
// they might have a command passed from a rule, and want to use the level from the group that triggered the rule (useful for syncing)
if(command.level == 'level') {
changeto = level;
}

if(changeto==100){
cmdtext = 'on';
}
else if (changeto==0) {
cmdtext = 'off';
}

// if they want to fade up slowly
if(command.delay) {
cmdtext = 'ramp';
}

sendLightingCommand(command.group,cmdtext,changeto,command.delay,DB.devices[command.group].vendor);
}

// TIMERS SUPPORT
if(command.timeout > 0 && command.level > 0){
// if there is already a timer running, lets clear it and start it up again
killTimer(command.group);

console.log('TIMER: will turn off group '+command.group+' in '+command.timeout+' seconds')
timers[command.group] = setTimeout(function(){
sendLightingCommand(command.group,'off',0,0,DB.devices[command.group].vendor);
}, command.timeout*1000);
}

function sendLightingCommand(device,command,level,delay,vendor){
if(vendor=='cbus'){
var cmd = CBUS.cmdString(command.group,'off',0,0);
CBUS.write(cmd);
}
// TODO: integration with other vendors here?
}
}

function killTimer(group) {
if(timers[group]) {
console.log('Killing TIMER: '+group);
clearTimeout(timers[group]);
}
}

function getTimeString(dd,tzoffset) {
// we may need to adjust these times based on the offset
dd = new Date(dd.getTime() - tzoffset*60000);

var hh = dd.getHours();
var mm = dd.getMinutes();

mm = ( mm < 10 ? "0" : "" ) + mm;
hh = ( hh < 10 ? "0" : "" ) + hh;

return hh + ":" + mm;
}
30 changes: 19 additions & 11 deletions config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
var config = {
cgate: {
host: '192.168.11.40',
contolport: 20023,
eventport: 20024,
statusport: 20025
},
webserver: {
port: 8080,
host: '0.0.0.0'
}
cgate: {
host: '192.168.11.40',
contolport: 20023,
eventport: 20024,
statusport: 20025,
cbusname: 'WEBB',
network: 254,
application: 56
},
webserver: {
port: 8080,
host: '0.0.0.0'
},
location: {
latitude: '43.4667',
longitude: '-112.0333',
timezone: 'America/Denver'
}
};

module.exports = config;
module.exports = config;
Loading

0 comments on commit 2c58370

Please sign in to comment.