homebridge-mcp-contact-sensor
Version:
a homebridge(https://github.com/nfarina/homebridge) plugin for a contact sensor and motion sensor via mcp23017 i2c port for Raspberry Pi.
393 lines (313 loc) • 12.8 kB
JavaScript
var mqtt = require("mqtt");
var
i2c = require('i2c-bus'),
i2c1 = i2c.openSync(1);
var
IODIRA = 0x00,
IODIRB = 0x01,
OLATA = 0x14,
OLATB = 0x15,
GPIOA = 0x12,
GPIOB = 0x13,
GPPUA = 0x0C,
GPPUB = 0x0D;
let Accessory, Service, Characteristic, UUIDGen;
module.exports = (homebridge) => {
// Accessory must be created from PlatformAccessory Constructor
Accessory = homebridge.platformAccessory;
// Service and Characteristic are from hap-nodejs
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
UUIDGen = homebridge.hap.uuid;
// For platform plugin to be considered as dynamic platform plugin,
// registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true
homebridge.registerPlatform('homebridge-mcp-sensors-platform', 'mcpSensors', mcpSensorsPlatform, true);
};
// Platform constructor
// config may be null
// api may be null if launched from old homebridge version
function mcpSensorsPlatform(log, config, api) {
this.log = log;
this.config = config;
this.accessories = [];
this.name = config.name;
this.url = config.url || '';
this.addresses = config.addresses || ['0x20'];
this.updateInterval = config.updateInterval || 1000;
this.debug = config.debug || false;
this.pins = config.pins || {};
this.states = {};
this.AlarmState = Characteristic.SecuritySystemCurrentState.DISARM;
this.AlarmTargetState = Characteristic.SecuritySystemCurrentState.DISARM;
for (var a in this.addresses) {
var adr = this.addresses[a];
this.states[adr] = ["", ""];
}
if (api) {
this.api = api;
this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this));
}
if (this.url!='') {
var clientId = 'mcpSensors_' + config.name.replace(/[^\x20-\x7F]/g, "") + '_' + Math.random().toString(16).substr(2, 8);
var options = {
keepalive: 10,
clientId: clientId,
protocolId: 'MQTT',
protocolVersion: 4,
clean: true,
reconnectPeriod: 1000,
connectTimeout: 30 * 1000,
will: {
topic: 'WillMsg',
payload: 'Connection Closed abnormally..!',
qos: 0,
retain: false
},
username: config.username,
password: config.password,
rejectUnauthorized: false
};
// connect to MQTT broker
this.mqttClient = mqtt.connect(this.url, this.options);
this.mqttClient.on('error', function (err) {
console.log('MQTT Error: ' + err);
});
this.mqttClient.on('connect', function () {
console.log('MQTT connect');
});
}
}
mcpSensorsPlatform.prototype.mqttPublish = function(topic, message) {
if (this.url!='') {
console.log("mcp publish topic: "+topic+ " mess: "+message.toString());
this.mqttClient.publish(topic, message.toString());
}
}
// Method to restore accessories from cache
mcpSensorsPlatform.prototype.configureAccessory = function (accessory) {
this.log("config accessory: " + accessory.context.name + "'...");
this.setService(accessory);
this.accessories[accessory.context.name] = accessory;
}
// Method to setup accesories from config.json
mcpSensorsPlatform.prototype.didFinishLaunching = function () {
this.log("Platform didFinishLaunching: " + this.pins + "'...");
// Add or update accessories defined in config.json
for (var i in this.pins) {
this.addAccessory(this.pins[i]);
}
this.log("Platform remove old accesories");
// Remove extra accessories in cache
for (var name in this.accessories) {
var accessory = this.accessories[name];
if (!accessory.reachable) {
this.removeAccessory(accessory);
this.log("Remove accessory: "+name);
}
}
this.log("Platform init i2c");
for (var a in this.addresses) {
this.log("init adres: " + this.addresses[a] + " ...");
this.address = parseInt(this.addresses[a], 16);
i2c1.writeByteSync(this.address, IODIRA, 0xFF);
i2c1.writeByteSync(this.address, OLATA, 0x00);
i2c1.writeByteSync(this.address, GPPUA, 0xFF);
i2c1.writeByteSync(this.address, IODIRB, 0xFF);
i2c1.writeByteSync(this.address, OLATB, 0x00);
i2c1.writeByteSync(this.address, GPPUB, 0xFF);
}
setInterval(function(sensors) {
var stateAlarmMotion = false;
var stateAlarmContants = false;
for (var a in sensors.addresses) {
sensors.address = parseInt(sensors.addresses[a], 16);
sensors.lastStateA = sensors.states[sensors.addresses[a]][0];
sensors.lastStateB = sensors.states[sensors.addresses[a]][1];
sensors.stateA = i2c1.readByteSync(sensors.address, GPIOA);
sensors.stateB = i2c1.readByteSync(sensors.address, GPIOB);
if (sensors.debug) {
console.log('MCP read: ' + sensors.address.toString(16) +' PIN A: ' + sensors.stateA.toString(2)+' last: '+ sensors.lastStateA.toString(2));
console.log('MCP read: ' + sensors.address.toString(16) +' PIN B: ' + sensors.stateB.toString(2)+' last: '+ sensors.lastStateB.toString(2));
}
if (sensors.lastStateA!=sensors.stateA) {
console.log('MCP - ' + sensors.address.toString(16) +' change on PIN A: ' + sensors.stateA.toString(2)+' last: '+ sensors.lastStateA.toString(2));
}
if (sensors.lastStateB!=sensors.stateB) {
console.log('MCP - ' + sensors.address.toString(16) +' change on PIN B: ' + sensors.stateB.toString(2)+' last: '+ sensors.lastStateB.toString(2));
}
for (var i in sensors.pins) {
var state ='';
var laststate = '';
var update=false;
if (sensors.pins[i].kind=='security') {
//todo: reading keypad for arm/disarm system
}
else {
if (sensors.pins[i].address==sensors.address) {
if (sensors.pins[i].port=='A') {
state=sensors.stateA;
laststate=sensors.lastStateA;
update=sensors.lastStateA!=sensors.stateA;
}
else {
state=sensors.stateB;
laststate=sensors.lastStateB;
update=sensors.lastStateB!=sensors.stateB;
}
if (update) {
var pinInt = Math.pow(2, sensors.pins[i].pin);
var pinState = (state & pinInt) != 0;
var pinLastState = (laststate & pinInt) != 0;
if (sensors.debug) {
console.log('MCP - update ' +sensors.address.toString(16) +' '+sensors.pins[i].port+ sensors.pins[i].pin + ' '+pinInt.toString()+' '+pinState.toString()+' '+pinLastState.toString()+' '+state.toString(2));
}
if (pinLastState!=pinState) {
var accessory = sensors.accessories[sensors.pins[i].name];
if (accessory) {
if (sensors.pins[i].kind=='motion') {
accessory.getService(Service.MotionSensor)
.getCharacteristic(Characteristic.MotionDetected)
.setValue(pinState);
if (pinState) {
stateAlarmMotion = true;
}
}
else {
accessory.getService(Service.ContactSensor)
.getCharacteristic(Characteristic.ContactSensorState)
.setValue(pinState);
if (pinState) {
stateAlarmContants = true;
}
}
if (sensors.pins[i].topic && sensors.pins[i].topic!='') {
sensors.mqttPublish(sensors.pins[i].topic, pinState ? 1 : 0);
}
}
}
}
}
}
sensors.states[sensors.addresses[a]][0] = sensors.stateA;
sensors.states[sensors.addresses[a]][1] = sensors.stateB;
}
if (sensors.SecuritySystem) {
//logs
stateAl = stateAlarmMotion || stateAlarmContants ? Characteristic.SecuritySystemCurrentState.TRIGGER : sensors.AlarmTargetState;
if (stateAl!=sensors.AlarmState) {
console.log("alarm state change " + stateAlarmMotion+" "+stateAlarmContants+" "+sensors.AlarmTargetState);
}
//alarm away logic:
if (sensors.AlarmState==Characteristic.SecuritySystemCurrentState.AWAY_ARM) {
stateAl = stateAlarmMotion || stateAlarmContants ? Characteristic.SecuritySystemCurrentState.TRIGGER: sensors.AlarmTargetState;
if (stateAl!=sensors.AlarmState) {
console.log("setting alarm for '" + sensors.pins[i].name + "'...");
sensors.AlarmState=stateAl;
sensors.SecuritySystem.setCharacteristic(Characteristic.SecuritySystemCurrentState, sensors.AlarmState);
}
}
}
}
}, this.updateInterval, this);
}
// Method to add and update HomeKit accessories
mcpSensorsPlatform.prototype.addAccessory = function (data) {
this.log("Initializing platform accessory '" + data.name + "'...");
// Retrieve accessory from cache
var accessory = this.accessories[data.name];
if (!accessory) {
this.log("creating accessory '" + data.name + "'...");
// Setup accessory as SWITCH (8) category.
var uuid = UUIDGen.generate(data.name);
accessory = new Accessory(data.name, uuid, 10);
if (data.kind=='motion') {
this.log("setting MotionSensor for '" + data.name + "'...");
accessory.addService(Service.MotionSensor, data.name);
}
else if (data.kind=='security') {
this.log("setting Security for '" + data.name + "'...");
this.SecuritySystem = accessory.addService(Service.SecuritySystem, data.name);
}
else {
this.log("setting ContactSensor for '" + data.name + "'...");
accessory.addService(Service.ContactSensor, data.name);
}
// New accessory is always reachable
accessory.reachable = true;
// Setup listeners for different switch events
this.setService(accessory);
// Register new accessory in HomeKit
this.api.registerPlatformAccessories("homebridge-mcp-sensors-platform", "mcpSensors", [accessory]);
// Store accessory in cache
this.accessories[data.name] = accessory;
}
if (data.manufacturer) data.manufacturer = data.manufacturer.toString();
if (data.model) data.model = data.model.toString();
if (data.serial) data.serial = data.serial.toString();
// Store and initialize variables into context
var cache = accessory.context;
cache.name = data.name;
cache.port = data.port;
cache.pin = data.pin;
cache.kind = data.kind;
// Retrieve initial state
this.getInitState(accessory);
}
// Method to remove accessories from HomeKit
mcpSensorsPlatform.prototype.removeAccessory = function (accessory) {
if (accessory) {
var name = accessory.context.name;
this.log(name + " is removed from HomeBridge.");
this.api.unregisterPlatformAccessories("homebridge-mcp-sensors-platform", "mcpSensors", [accessory]);
delete this.accessories[name];
}
}
// Method to setup listeners for different events
mcpSensorsPlatform.prototype.setService = function (accessory) {
this.log(accessory.context.name + " setService "+ accessory.context.kind);
if (accessory.context.kind=='security') {
this.SecuritySystem = accessory.getService(Service.SecuritySystem);
this.SecuritySystem.getCharacteristic(Characteristic.SecuritySystemCurrentState)
.on("get", this.getAlarmCurrentState.bind(this))
.setValue(this.AlarmState);
this.SecuritySystem.getCharacteristic(Characteristic.SecuritySystemTargetState)
.on("get", this.getAlarmTargetState.bind(this))
.on("set", this.setAlarmTargetState.bind(this))
.setValue(this.AlarmTargetState);
}
accessory.on('identify', this.identify.bind(this, accessory.context));
}
// Method to retrieve initial state
mcpSensorsPlatform.prototype.getInitState = function (accessory) {
var manufacturer = accessory.context.manufacturer || "MCP-kakaki";
var model = accessory.context.model || "MCP-Contact";
var serial = accessory.context.serial || "123-456";
// Update HomeKit accessory information
accessory.getService(Service.AccessoryInformation)
.setCharacteristic(Characteristic.Manufacturer, manufacturer)
.setCharacteristic(Characteristic.Model, model)
.setCharacteristic(Characteristic.SerialNumber, serial);
// Configured accessory is reachable
accessory.updateReachability(true);
}
// Method to handle identify request
mcpSensorsPlatform.prototype.identify = function (thisSwitch, paired, callback) {
this.log(thisSwitch.name + " identify requested!");
callback();
}
//alarm
mcpSensorsPlatform.prototype.getAlarmCurrentState = function(callback) {
this.log("MCP-Alarm get current state "+this.AlarmState);
callback(null, this.AlarmState);
};
mcpSensorsPlatform.prototype.getAlarmTargetState = function(callback) {
this.log("MCP-Alarm get target state "+this.AlarmTargetState);
callback(null, this.AlarmTargetState);
};
mcpSensorsPlatform.prototype.setAlarmTargetState = function(state, callback) {
this.log("MCP-Alarm set target state to "+state);
this.AlarmTargetState = state;
this.AlarmState = state;
this.SecuritySystem.setCharacteristic(Characteristic.SecuritySystemCurrentState, state);
callback(null, state);
}