diff --git a/README.md b/README.md index f11dd75..641e32f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [Home Assistant](http://home-assistant.io) [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. diff --git a/accessories/FileSensor.js b/accessories/FileSensor.js new file mode 100644 index 0000000..e377dc6 --- /dev/null +++ b/accessories/FileSensor.js @@ -0,0 +1,76 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var chokidar = require("chokidar"); +var debug = require("debug")("FileSensorAccessory"); +var crypto = require("crypto"); + +module.exports = { + accessory: FileSensorAccessory +} + +function FileSensorAccessory(log, config) { + this.log = log; + + // url info + this.name = config["name"]; + this.path = config["path"]; + this.window_seconds = config["window_seconds"] || 5; + this.sensor_type = config["sensor_type"] || "m"; + this.inverse = config["inverse"] || false; + + if(config["sn"]){ + this.sn = config["sn"]; + } else { + var shasum = crypto.createHash('sha1'); + shasum.update(this.path); + this.sn = shasum.digest('base64'); + debug('Computed SN ' + this.sn); + } +} + +FileSensorAccessory.prototype = { + + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Homebridge") + .setCharacteristic(Characteristic.Model, "File Sensor") + .setCharacteristic(Characteristic.SerialNumber, this.sn); + + var service, changeAction; + if(this.sensor_type === "c"){ + service = new Service.ContactSensor(); + changeAction = function(newState){ + service.getCharacteristic(Characteristic.ContactSensorState) + .setValue(newState ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED); + }; + } else { + service = new Service.MotionSensor(); + changeAction = function(newState){ + service.getCharacteristic(Characteristic.MotionDetected) + .setValue(newState); + }; + } + + var changeHandler = function(path, stats){ + var d = new Date(); + if(d.getTime() - stats.mtime.getTime() <= (this.window_seconds * 1000)){ + var newState = this.inverse ? false : true; + changeAction(newState); + if(this.timer !== undefined) clearTimeout(this.timer); + this.timer = setTimeout(function(){changeAction(!newState);}, this.window_seconds * 1000); + } + }.bind(this); + + var watcher = chokidar.watch(this.path, {alwaysStat: true}); + watcher.on('add', changeHandler); + watcher.on('change', changeHandler); + + return [informationService, service]; + } +}; diff --git a/accessories/GenericRS232Device.js b/accessories/GenericRS232Device.js new file mode 100644 index 0000000..b84e4cc --- /dev/null +++ b/accessories/GenericRS232Device.js @@ -0,0 +1,58 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var SerialPort = require("serialport").SerialPort; + +module.exports = { + accessory: GenericRS232DeviceAccessory +} + +function GenericRS232DeviceAccessory(log, config) { + this.log = log; + this.id = config["id"]; + this.name = config["name"]; + this.model_name = config["model_name"]; + this.manufacturer = config["manufacturer"]; + this.on_command = config["on_command"]; + this.off_command = config["off_command"]; + this.device = config["device"]; + this.baudrate = config["baudrate"]; +} + +GenericRS232DeviceAccessory.prototype = { + setPowerState: function(powerOn, callback) { + var that = this; + var command = powerOn ? that.on_command : that.off_command; + var serialPort = new SerialPort(that.device, { baudrate: that.baudrate }, false); + serialPort.open(function (error) { + if (error) { + callback(new Error('Can not communicate with ' + that.name + " (" + error + ")")) + } else { + serialPort.write(command, function(err, results) { + if (error) { + callback(new Error('Can not send power command to ' + that.name + " (" + err + ")")) + } else { + callback() + } + }); + } + }); + }, + + getServices: function() { + var switchService = new Service.Switch(this.name); + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) + .setCharacteristic(Characteristic.Model, this.model_name) + .setCharacteristic(Characteristic.SerialNumber, this.id); + + switchService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + + return [informationService, switchService]; + } +} + +module.exports.accessory = GenericRS232DeviceAccessory; \ No newline at end of file diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index b5fd023..04414af 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -1,196 +1,73 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require('HAP-NodeJS').Service; +var Characteristic = require('HAP-NodeJS').Characteristic; var request = require("request"); +module.exports = { + accessory: LockitronAccessory +} + function LockitronAccessory(log, config) { this.log = log; this.name = config["name"]; - this.lockID = config["lock_id"]; this.accessToken = config["api_token"]; + this.lockID = config["lock_id"]; } -LockitronAccessory.prototype = { - getState: function(callback) { - this.log("Getting current state..."); - - var that = this; - - var query = { - access_token: this.accessToken - }; - - request.get({ - url: "https://api.lockitron.com/v2/locks/"+this.lockID, - qs: query - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - var json = JSON.parse(body); - var state = json.state; // "lock" or "unlock" - var locked = state == "lock" - callback(locked); - } - else { - that.log("Error getting state (status code "+response.statusCode+"): " + err) - callback(undefined); - } - }); - }, +LockitronAccessory.prototype.getState = function(callback) { + this.log("Getting current state..."); - setState: function(state) { - this.log("Set state to " + state); + request.get({ + url: "https://api.lockitron.com/v2/locks/"+this.lockID, + qs: { access_token: this.accessToken } + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + var json = JSON.parse(body); + var state = json.state; // "lock" or "unlock" + this.log("Lock state is %s", state); + var locked = state == "lock" + callback(null, locked); // success + } + else { + this.log("Error getting state (status code %s): %s", response.statusCode, err); + callback(err); + } + }.bind(this)); +} + +LockitronAccessory.prototype.setState = function(state, callback) { + var lockitronState = (state == 1) ? "lock" : "unlock"; - var lockitronState = (state == 1) ? "lock" : "unlock"; - var that = this; + this.log("Set state to %s", lockitronState); - var query = { - access_token: this.accessToken, - state: lockitronState - }; + request.put({ + url: "https://api.lockitron.com/v2/locks/"+this.lockID, + qs: { access_token: this.accessToken, state: lockitronState } + }, function(err, response, body) { - request.put({ - url: "https://api.lockitron.com/v2/locks/"+this.lockID, - qs: query - }, function(err, response, body) { + if (!err && response.statusCode == 200) { + this.log("State change complete."); + callback(null); // success + } + else { + this.log("Error '%s' setting lock state. Response: %s", err, body); + callback(err || new Error("Error setting lock state.")); + } + }.bind(this)); +}, - if (!err && response.statusCode == 200) { - that.log("State change complete."); - } - else { - that.log("Error '"+err+"' setting lock state: " + body); - } - }); - }, - - getServices: function() { - var that = this; - return [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Apigee", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-2", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "A1S2NASF88EW", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - },{ - sType: types.LOCK_MECHANISM_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Lock Mechanism", - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.CURRENT_LOCK_MECHANISM_STATE_CTYPE, - onRead: function(callback) { that.getState(callback); }, - onUpdate: function(value) { that.log("Update current state to " + value); }, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - designedMaxLength: 1 - },{ - cType: types.TARGET_LOCK_MECHANISM_STATE_CTYPE, - onUpdate: function(value) { that.setState(value); }, - perms: ["pr","pw","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }] - },{ - sType: types.LOCK_MANAGEMENT_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Lock Management", - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE, - onUpdate: function(value) { that.log("Update control point to " + value); }, - perms: ["pw"], - format: "data", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMaxLength: 255 - },{ - cType: types.VERSION_CTYPE, - onUpdate: function(value) { that.log("Update version to " + value); }, - perms: ["pr"], - format: "string", - initialValue: "1.0", - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMaxLength: 255 - }] - }]; - } -}; - -module.exports.accessory = LockitronAccessory; \ No newline at end of file +LockitronAccessory.prototype.getServices = function() { + + var service = new Service.LockMechanism(this.name); + + service + .getCharacteristic(Characteristic.LockCurrentState) + .on('get', this.getState.bind(this)); + + service + .getCharacteristic(Characteristic.LockTargetState) + .on('get', this.getState.bind(this)) + .on('set', this.setState.bind(this)); + + return [service]; +} diff --git a/accessories/WeMo.js b/accessories/WeMo.js index df16f56..c19e19f 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -140,9 +140,7 @@ WeMoAccessory.prototype.getServices = function() { garageDoorService .getCharacteristic(Characteristic.TargetDoorState) - .on('set', this.setTargetDoorState.bind(this)) - .supportsEventNotification = false; - + .on('set', this.setTargetDoorState.bind(this)); return [garageDoorService]; } diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js index efdbd4e..824b9dd 100644 --- a/accessories/knxdevice.js +++ b/accessories/knxdevice.js @@ -1,6 +1,13 @@ /* * This is a KNX universal accessory shim. + * This is NOT the version for dynamic installation * +New 2015-09-16: Welcome iOS9.0 +new features includ: +services: +Window +WindowCovering +ContactSensor * */ var Service = require("HAP-NodeJS").Service; @@ -23,7 +30,7 @@ function KNXDevice(log, config) { if (config.knxd_ip){ this.knxd_ip = config.knxd_ip; } else { - throw new Error("MISSING KNXD IP"); + throw new Error("KNX configuration fault: MISSING KNXD IP"); } if (config.knxd_port){ this.knxd_port = config.knxd_port; @@ -87,7 +94,7 @@ KNXDevice.prototype = { this.log("[ERROR] knxwrite:sendAPDU: " + err); callback(err); } else { - // this.log("knx data sent"); + this.log("knx data sent: Value "+value+ " for GA "+groupAddress); callback(); } }.bind(this)); @@ -160,7 +167,7 @@ KNXDevice.prototype = { knxregister_bool: function(addresses, characteristic) { this.log("knx registering BOOLEAN " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type + " for " + characteristic.displayName); // iterate(characteristic); characteristic.setValue(val ? 1 : 0, undefined, 'fromKNXBus'); }.bind(this)); @@ -168,7 +175,7 @@ KNXDevice.prototype = { knxregister_boolReverse: function(addresses, characteristic) { this.log("knx registering BOOLEAN " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type + " for " + characteristic.displayName); // iterate(characteristic); characteristic.setValue(val ? 0 : 1, undefined, 'fromKNXBus'); }.bind(this)); @@ -177,7 +184,7 @@ KNXDevice.prototype = { knxregister_percent: function(addresses, characteristic) { this.log("knx registering PERCENT " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type+ " for " + characteristic.displayName); if (type !== "DPT5") { this.log("[ERROR] Received value cannot be a percentage value"); } else { @@ -199,7 +206,7 @@ KNXDevice.prototype = { knxregister_float: function(addresses, characteristic) { this.log("knx registering FLOAT " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type+ " for " + characteristic.displayName); var hk_value = Math.round(val*10)/10; if (hk_value>=characteristic.minimumValue && hk_value<=characteristic.maximumValue) { characteristic.setValue(hk_value, undefined, 'fromKNXBus'); // 1 decoimal for HomeKit @@ -214,7 +221,7 @@ KNXDevice.prototype = { knxregister_HVAC: function(addresses, characteristic) { this.log("knx registering HVAC " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type+ " for " + characteristic.displayName); var HAPvalue = 0; switch (val){ case 0: @@ -256,7 +263,7 @@ KNXDevice.prototype = { knxregister: function(addresses, characteristic) { this.log("knx registering " + addresses); knxd_registerGA(addresses, function(val, src, dest, type){ - this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName); + this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type "+type+ " for " + characteristic.displayName); characteristic.setValue(val, undefined, 'fromKNXBus'); }.bind(this)); }, @@ -552,16 +559,26 @@ KNXDevice.prototype = { // TargetTemperature if available if (config.TargetTemperature) { this.log("Thermostat TargetTemperature characteristic enabled"); + + // DEBUG + console.log("default value: " + myService.getCharacteristic(Characteristic.TargetTemperature).value); + // DEBUG + // default boundary too narrow for thermostats myService.getCharacteristic(Characteristic.TargetTemperature).minimumValue=0; // Β°C myService.getCharacteristic(Characteristic.TargetTemperature).maximumValue=40; // Β°C this.bindCharacteristic(myService, Characteristic.TargetTemperature, "Float", config.TargetTemperature); } - // HVAC missing yet + // HVAC if (config.CurrentHeatingCoolingState) { this.log("Thermostat CurrentHeatingCoolingState characteristic enabled"); this.bindCharacteristic(myService, Characteristic.CurrentHeatingCoolingState, "HVAC", config.CurrentHeatingCoolingState); } + // HVAC + if (config.TargetHeatingCoolingState) { + this.log("Thermostat TargetHeatingCoolingState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.TargetHeatingCoolingState, "HVAC", config.TargetHeatingCoolingState); + } return myService; }, @@ -584,13 +601,174 @@ KNXDevice.prototype = { var myService = new Service.TemperatureSensor(config.name,config.name); // CurrentTemperature) if (config.CurrentTemperature) { - this.log("Thermostat CurrentTemperature characteristic enabled"); + this.log("TemperatureSensor CurrentTemperature characteristic enabled"); this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature); } return myService; }, + + // window type (iOS9 assumed) + getWindowService: function(config) { +// Service.Window = function(displayName, subtype) { +// Service.call(this, displayName, '0000008B-0000-1000-8000-0026BB765291', subtype); +// +// // Required Characteristics +// this.addCharacteristic(Characteristic.CurrentPosition); +// this.addCharacteristic(Characteristic.TargetPosition); +// this.addCharacteristic(Characteristic.PositionState); +// +// // Optional Characteristics +// this.addOptionalCharacteristic(Characteristic.HoldPosition); +// this.addOptionalCharacteristic(Characteristic.ObstructionDetected); +// this.addOptionalCharacteristic(Characteristic.Name); + + // Characteristic.PositionState.DECREASING = 0; +// Characteristic.PositionState.INCREASING = 1; +// Characteristic.PositionState.STOPPED = 2; + + + // some sanity checks + + + if (config.type !== "Window") { + this.log("[ERROR] Window Service for non 'Window' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] Window Service without 'name' property called"); + return undefined; + } + var myService = new Service.Window(config.name,config.name); + + if (config.CurrentPosition) { + this.log("Window CurrentPosition characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.CurrentPosition, "Percent", config.CurrentPosition); + } + if (config.TargetPosition) { + this.log("Window TargetPosition characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.TargetPosition, "Percent", config.TargetPosition); + } + if (config.PositionState) { + this.log("Window PositionState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.PositionState, "Float", config.PositionState); + } + return myService; + }, + + +// /** +// * Service "Window Covering" +// */ +// +// Service.WindowCovering = function(displayName, subtype) { +// Service.call(this, displayName, '0000008C-0000-1000-8000-0026BB765291', subtype); +// +// // Required Characteristics +// this.addCharacteristic(Characteristic.CurrentPosition); +// this.addCharacteristic(Characteristic.TargetPosition); +// this.addCharacteristic(Characteristic.PositionState); +// +// // Optional Characteristics +// this.addOptionalCharacteristic(Characteristic.HoldPosition); +// this.addOptionalCharacteristic(Characteristic.TargetHorizontalTiltAngle); +// this.addOptionalCharacteristic(Characteristic.TargetVerticalTiltAngle); +// this.addOptionalCharacteristic(Characteristic.CurrentHorizontalTiltAngle); +// this.addOptionalCharacteristic(Characteristic.CurrentVerticalTiltAngle); +// this.addOptionalCharacteristic(Characteristic.ObstructionDetected); +// this.addOptionalCharacteristic(Characteristic.Name); +// }; + getWindowCoveringService: function(config) { + + // some sanity checks + + + if (config.type !== "WindowCovering") { + this.log("[ERROR] WindowCovering Service for non 'WindowCovering' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] WindowCovering Service without 'name' property called"); + return undefined; + } + var myService = new Service.WindowCovering(config.name,config.name); + + if (config.CurrentPosition) { + this.log("WindowCovering CurrentPosition characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.CurrentPosition, "Percent", config.CurrentPosition); + } + if (config.TargetPosition) { + this.log("WindowCovering TargetPosition characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.TargetPosition, "Percent", config.TargetPosition); + } + if (config.PositionState) { + this.log("WindowCovering PositionState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.PositionState, "Float", config.PositionState); + } + return myService; + }, + +// Service.ContactSensor = function(displayName, subtype) { +// Service.call(this, displayName, '00000080-0000-1000-8000-0026BB765291', subtype); +// +// // Required Characteristics +// this.addCharacteristic(Characteristic.ContactSensorState); +// +// // Optional Characteristics +// this.addOptionalCharacteristic(Characteristic.StatusActive); +// this.addOptionalCharacteristic(Characteristic.StatusFault); +// this.addOptionalCharacteristic(Characteristic.StatusTampered); +// this.addOptionalCharacteristic(Characteristic.StatusLowBattery); +// this.addOptionalCharacteristic(Characteristic.Name); +// }; +// Characteristic.ContactSensorState.CONTACT_DETECTED = 0; +// Characteristic.ContactSensorState.CONTACT_NOT_DETECTED = 1; + getContactSenserService: function(config) { + // some sanity checks + if (config.type !== "ContactSensor") { + this.log("[ERROR] ContactSensor Service for non 'ContactSensor' service called"); + return undefined; + } + if (!config.name) { + this.log("[ERROR] ContactSensor Service without 'name' property called"); + return undefined; + } + var myService = new Service.ContactSensor(config.name,config.name); + + if (config.ContactSensorState) { + this.log("ContactSensor ContactSensorState characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.ContactSensorState, "Bool", config.ContactSensorState); + } else if (config.ContactSensorStateContact1) { + this.log("ContactSensor ContactSensorStateContact1 characteristic enabled"); + this.bindCharacteristic(myService, Characteristic.ContactSensorState, "BoolReverse", config.ContactSensorStateContact1); + } + //optionals + if (config.StatusActive) { + this.log("ContactSensor StatusActive characteristic enabled"); + myService.addCharacteristic(Characteristic.StatusActive); + this.bindCharacteristic(myService, Characteristic.StatusActive, "Bool", config.StatusActive); + } + if (config.StatusFault) { + this.log("ContactSensor StatusFault characteristic enabled"); + myService.addCharacteristic(Characteristic.StatusFault); + this.bindCharacteristic(myService, Characteristic.StatusFault, "Bool", config.StatusFault); + } + if (config.StatusTampered) { + this.log("ContactSensor StatusTampered characteristic enabled"); + myService.addCharacteristic(Characteristic.StatusTampered); + this.bindCharacteristic(myService, Characteristic.StatusTampered, "Bool", config.StatusTampered); + } + if (config.StatusLowBattery) { + this.log("ContactSensor StatusLowBattery characteristic enabled"); + myService.addCharacteristic(Characteristic.StatusLowBattery); + this.bindCharacteristic(myService, Characteristic.StatusLowBattery, "Bool", config.StatusLowBattery); + } + return myService; + }, + + + /* assemble the device ***************************************************************************************************/ @@ -625,7 +803,11 @@ KNXDevice.prototype = { this.log("[ERROR] must specify 'type' and 'name' properties for each service in config.json. KNX platform section fault "); throw new Error("Must specify 'type' and 'name' properties for each service in config.json"); } + this.log("Preparing Service: " + int + " of type "+configService.type) switch (configService.type) { + case "ContactSensor": + accessoryServices.push(this.getContactSenserService(configService)); + break; case "Lightbulb": accessoryServices.push(this.getLightbulbService(configService)); break; @@ -638,8 +820,14 @@ KNXDevice.prototype = { case "Thermostat": accessoryServices.push(this.getThermostatService(configService)); break; + case "Window": + accessoryServices.push(this.getWindowService(configService)); + break; + case "WindowCovering": + accessoryServices.push(this.getWindowCoveringService(configService)); + break; default: - this.log("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault "); + this.log("[ERROR] unknown 'type' property of '"+configService.type+"' for service "+ configService.name + " in config.json. KNX platform section fault "); //throw new Error("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault "); } } diff --git a/accessories/knxdevice.md b/accessories/knxdevice.md new file mode 100644 index 0000000..c9131cc --- /dev/null +++ b/accessories/knxdevice.md @@ -0,0 +1,126 @@ +# Syntax of the config.json +In the platforms section, you can insert a KNX type platform. +You need to configure all devices directly in the config.json. + + "platforms": [ + { + "platform": "KNX", + "name": "KNX", + "knxd_ip": "192.168.178.205", + "knxd_port": 6720, + "accessories": [ + { + "accessory_type": "knxdevice", + "name": "Living Room North Lamp", + "services": [ + { + "type": "Lightbulb", + "description": "iOS8 Lightbulb type, supports On (Switch) and Brightness", + "name": "Living Room North Lamp", + "On": { + "Set": "1/1/6", + "Listen": [ + "1/1/63" + ] + }, + "Brightness": { + "Set": "1/1/62", + "Listen": [ + "1/1/64" + ] + } + } + ] + } + } + +In the accessories section (the array within the brackets [ ]) you can insert as many objects as you like in the following form + { + "accessory_type": "knxdevice", + "name": "Here goes your display name, this will be shown in HomeKit apps", + "services": [ + { + } + ] + } + +You have to add services in the following syntax: + { + "type": "SERVICENAME", + "description": "This is just for you to remember things", + "name": "We need a name for each service, though it usually shows only if multiple services are present in one accessory", + "CHARACTERISTIC1": { + "Set": "1/1/6", + "Listen": [ + "1/1/63" + ] + }, + "CHARACTERISTIC2": { + "Set": "1/1/62", + "Listen": [ + "1/1/64" + ] + } + } +CHARACTERISTIC are properties that are dependent on the service type, so they are listed below. +Two kinds of addresses are supported: "Set":"1/2/3" is a writable group address, to which changes are sent if the service supports changing values. Changes on the bus are listened to, too. +"Listen":["1/2/3","1/2/4","1/2/5"] is an array of addresses that are listened to additionally. To these addresses never values get written, but the on startup the service will issue read requests to ALL addresses listed in Set: and in Listen: + + +# Supported Services and their characteristics + +## Lightbulb + On: DPT 1, 1 as on, 0 as off + Brightness: DPT5 percentage, 100% (=255) the brightest + + +## LockMechanism +LockCurrentState: DPT 1, 1 as secured +OR (but not both:) +LockCurrentStateSecured0: DPT 1, 0 as secured + +LockTargetState: DPT 1, 1 as secured +LockTargetStateSecured0: DPT 1, 0 as secured + +## Thermostat +CurrentTemperature: DPT9 in °C [listen only] +TargetTemperature: DPT9, values 0..40°C only, all others are ignored +CurrentHeatingCoolingState: DPT5 HVAC, because of the incompatible mapping only off and heating (=auto) are shown, [listen only] +TargetHeatingCoolingState: as above + + +## TemperatureSensor +CurrentTemperature: DPT9 in °C [listen only] + +## Window +CurrentPosition: DPT5 percentage +TargetPosition: DPT5 percentage +PositionState: DPT5 value [listen only] + +## WindowCovering +CurrentPosition: DPT5 percentage +TargetPosition: DPT5 percentage +PositionState: DPT5 value [listen only] + +### not yet supported +HoldPosition +TargetHorizontalTiltAngle +TargetVerticalTiltAngle +CurrentHorizontalTiltAngle +CurrentVerticalTiltAngle +ObstructionDetected + +## ContactSensor +ContactSensorState: DPT 1, 0 as contact +OR +ContactSensorStateContact1: DPT 1, 1 as contact + +StatusActive: DPT 1, 1 as true +StatusFault: DPT 1, 1 as true +StatusTampered: DPT 1, 1 as true +StatusLowBattery: DPT 1, 1 as true + + +# DISCLAIMER +This is work in progress! + diff --git a/accessories/mpdclient.js b/accessories/mpdclient.js new file mode 100644 index 0000000..8fee5eb --- /dev/null +++ b/accessories/mpdclient.js @@ -0,0 +1,89 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var request = require("request"); +var komponist = require('komponist') + +module.exports = { + accessory: MpdClient +} + +function MpdClient(log, config) { + this.log = log; + this.host = config["host"] || 'localhost'; + this.port = config["port"] || 6600; +} + +MpdClient.prototype = { + + setPowerState: function(powerOn, callback) { + + var log = this.log; + + komponist.createConnection(this.port, this.host, function(error, client) { + + if (error) { + return callback(error); + } + + if (powerOn) { + client.play(function(error) { + log("start playing"); + client.destroy(); + callback(error); + }); + } else { + client.stop(function(error) { + log("stop playing"); + client.destroy(); + callback(error); + }); + } + + }); + }, + + getPowerState: function(callback) { + + komponist.createConnection(this.port, this.host, function(error, client) { + + if (error) { + return callback(error); + } + + client.status(function(error, status) { + + client.destroy(); + + if (status['state'] == 'play') { + callback(error, 1); + } else { + callback(error, 0); + } + }); + + }); + }, + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getServices: function() { + + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "MPD") + .setCharacteristic(Characteristic.Model, "MPD Client") + .setCharacteristic(Characteristic.SerialNumber, "81536334"); + + var switchService = new Service.Switch(); + + switchService.getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + return [informationService, switchService]; + } +}; diff --git a/app.js b/app.js index 8a92123..a8c2006 100644 --- a/app.js +++ b/app.js @@ -191,6 +191,16 @@ function createAccessory(accessoryInstance, displayName) { } } +// Returns the setup code in a scannable format. +function printPin(pin) { + console.log("Scan this code with your HomeKit App on your iOS device:"); + console.log("\x1b[30;47m%s\x1b[0m", " "); + console.log("\x1b[30;47m%s\x1b[0m", " β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” "); + console.log("\x1b[30;47m%s\x1b[0m", " β”‚ " + pin + " β”‚ "); + console.log("\x1b[30;47m%s\x1b[0m", " β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ "); + console.log("\x1b[30;47m%s\x1b[0m", " "); +} + // Returns a logging function that prepends messages with the given name in [brackets]. function createLog(name) { return function(message) { @@ -201,6 +211,7 @@ function createLog(name) { } function publish() { + printPin(bridgeConfig.pin); bridge.publish({ username: bridgeConfig.username || "CC:22:3D:E3:CE:30", port: bridgeConfig.port || 51826, diff --git a/config-sample.json b/config-sample.json index d684c9d..6f24554 100644 --- a/config-sample.json +++ b/config-sample.json @@ -72,6 +72,14 @@ "play_volume": -35, "setMainInputTo": "AirPlay" }, + { + "platform": "ZWayServer", + "url": "http://192.168.1.10:8083/", + "login": "zwayusername", + "password": "zwayuserpassword", + "poll_interval": 2, + "split_services": false + }, { "platform": "MiLight", "name": "MiLight", @@ -81,6 +89,13 @@ "delay": 30, "repeat": 3, "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] + }, + { + "platform": "HomeAssistant", + "name": "HomeAssistant", + "host": "http://192.168.1.10:8123", + "password": "XXXXX", + "supported_types": ["light", "switch", "media_player", "scene"] } ], @@ -172,9 +187,36 @@ { "accessory": "Hyperion", "name": "TV Backlight", - "description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion" + "description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion", "host": "localhost", "port": "19444" + }, + { + "accessory": "mpdclient", + "name" : "mpd", + "host" : "localhost", + "port" : 6600, + "description": "Allows some control of an MPD server" + }, + { + "accessory": "FileSensor", + "name": "File Time Motion Sensor", + "path": "/tmp/CameraDump/", + "window_seconds": 5, + "sensor_type": "m", + "inverse": false + }, + { + "accessory": "GenericRS232Device", + "name": "Projector", + "description": "Make sure you set a 'Siri-Name' for your iOS-Device (example: 'Home Cinema') otherwise it might not work.", + "id": "TYDYMU044UVNP", + "baudrate": 9600, + "device": "/dev/tty.usbserial", + "manufacturer": "Acer", + "model_name": "H6510BD", + "on_command": "* 0 IR 001\r", + "off_command": "* 0 IR 002\r" } ] } diff --git a/package.json b/package.json index 3732b9d..d0eb41c 100644 --- a/package.json +++ b/package.json @@ -13,19 +13,22 @@ "dependencies": { "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", "carwingsjs": "0.0.x", + "chokidar": "^1.0.5", "color": "0.10.x", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#0030b35856e04ee2b42f0d05839feaa5c44cbd1f", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", - "lifx": "https://github.com/magicmonkey/lifxjs.git", + "lifx": "git+https://github.com/magicmonkey/lifxjs.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", + "q": "1.4.x", + "tough-cookie": "^2.0.0", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", @@ -35,6 +38,8 @@ "wink-js": "0.0.5", "xml2js": "0.4.x", "xmldoc": "0.1.x", - "yamaha-nodejs": "0.4.x" + "komponist" : "0.1.0", + "yamaha-nodejs": "0.4.x", + "debug": "^2.2.0" } } diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index de5b466..8930011 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -8,6 +8,9 @@ // - Added support for Scenes // - Sorting device names // +// 22 July 2015 [lukeredpath] +// - Added SSL and basic auth support +// // 26 August 2015 [EddyK69] // - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes // - Fixed issue with dimmer-range; was 0-100, should be 0-16 @@ -17,6 +20,10 @@ // - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches // (Cannot determine if 'on/off'-type device is a Light or a Switch :( ) // +// 14 September 2015 [lukeredpath] +// - Fixed incorrect dimmer range for LightwaveRF lights (0-32 required, MaxDimLevel should be honored) +// +// // Domoticz JSON API required // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches // @@ -183,9 +190,7 @@ DomoticzAccessory.prototype = { url = this.platform.urlForQuery("type=command¶m=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false"); } else if (c == "setLevel") { - //Range should be 0-16 instead of 0-100 - //See http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_a_dimmable_light_to_a_certain_level - value = Math.round((value / 100) * 16) + value = this.dimmerLevelForValue(value) url = this.platform.urlForQuery("type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value); } else if (value != undefined) { @@ -208,11 +213,19 @@ DomoticzAccessory.prototype = { that.log("There was a problem sending command " + c + " to" + that.name); that.log(url); } else { - that.log(that.name + " sent command " + c); + that.log(that.name + " sent command " + c + " (value: " + value + ")"); } }) }, + // translates the HomeKit dim level as a percentage to whatever scale the device requires + dimmerLevelForValue: function(value) { + if (this.MaxDimLevel == 100) { + return value; + } + return Math.round((value / 100.0) * this.MaxDimLevel) + }, + informationCharacteristics: function() { return [ { @@ -362,4 +375,4 @@ DomoticzAccessory.prototype = { }; module.exports.accessory = DomoticzAccessory; -module.exports.platform = DomoticzPlatform; \ No newline at end of file +module.exports.platform = DomoticzPlatform; diff --git a/platforms/FHEM.js b/platforms/FHEM.js index ada3c47..03f3ee1 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -16,7 +16,11 @@ // When you attempt to add a device, it will ask for a "PIN code". // The default code for all HomeBridge accessories is 031-45-154. +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; + var types = require('HAP-NodeJS/accessories/types.js'); + var util = require('util'); @@ -38,31 +42,19 @@ FHEM_update(inform_id, value, no_update) { || FHEM_cached[inform_id] === value ) return; - //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { - // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; - - // if( info.username ) { - // var accessory = subscription.accessory; - // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - // //accessory.execute( cmd ); - - // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; - // } - //} - FHEM_cached[inform_id] = value; //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); if( !no_update ) - subscription.characteristic.updateValue(value, null); + subscription.characteristic.setValue(value, undefined, 'fromFhem'); } } -var FHEM_lastEventTimestamp; +var FHEM_lastEventTime; var FHEM_longpoll_running = false; -//FIXME: force reconnect on xxx bytes received ?, add filter, add since +//FIXME: add filter function FHEM_startLongpoll(connection) { if( FHEM_longpoll_running ) return; @@ -70,6 +62,8 @@ function FHEM_startLongpoll(connection) { var filter = ".*"; var since = "null"; + if( FHEM_lastEventTime ) + since = FHEM_lastEventTime/1000; var query = "/fhem.pl?XHR=1"+ "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ "×tamp="+Date.now() @@ -85,8 +79,9 @@ function FHEM_startLongpoll(connection) { return; input += data; + var lastEventTime = Date.now(); for(;;) { - var nOff = input.indexOf("\n", FHEM_longpollOffset); + var nOff = input.indexOf('\n', FHEM_longpollOffset); if(nOff < 0) break; var l = input.substr(FHEM_longpollOffset, nOff-FHEM_longpollOffset); @@ -113,14 +108,12 @@ function FHEM_startLongpoll(connection) { var subscription = FHEM_subscriptions[d[0]]; if( subscription != undefined ) { //console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); - FHEM_lastEventTimestamp = Date.now(); + FHEM_lastEventTime = lastEventTime; var accessory = subscription.accessory; var value = d[1]; if( value.match( /^set-/ ) ) continue; - if( value.match( /^set_/ ) ) - continue; var match = d[0].match(/([^-]*)-(.*)/); var device = match[1]; @@ -140,12 +133,12 @@ function FHEM_startLongpoll(connection) { continue; } else if( accessory.mappings.lock ) { - var lock = 0; + var lock = Characteristic.LockCurrentState.UNSECURED; if( value.match( /^locked/ ) ) - lock = 1; + lock = Characteristic.LockCurrentState.SECURED; if( value.match( /uncertain/ ) ) - level = 4; + level = Characteristic.LockCurrentState.UNKNOWN; FHEM_update( accessory.mappings.lock.informId, lock ); continue; @@ -333,12 +326,19 @@ FHEMPlatform.prototype = { var foundAccessories = []; + // mechanism to ensure callback is only executed once all requests complete + var asyncCalls = 0; + function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); } + var cmd = 'jsonlist2'; if( this.filter ) cmd += " " + this.filter; var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); this.log( 'fetching: ' + url ); + + asyncCalls++; + var that = this; this.connection.request.get( { url: url, json: true, gzip: true }, function(err, response, json) { @@ -388,17 +388,22 @@ FHEMPlatform.prototype = { } else if( s.Readings.humidity ) { accessory = new FHEMAccessory(that.log, that.connection, s); + } else if( s.Readings.voc ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + } else { that.log( 'ignoring ' + s.Internals.NAME ); } - if( accessory ) + if( accessory && Object.getOwnPropertyNames(accessory).length ) foundAccessories.push(accessory); }); } - callback(foundAccessories); + + //callback(foundAccessories); + callbackLater(); } else { that.log("There was a problem connecting to FHEM (1)."); @@ -417,6 +422,20 @@ FHEMAccessory(log, connection, s) { //log("got json: " + util.inspect(s) ); //log("got json: " + util.inspect(s.Internals) ); + if( !(this instanceof FHEMAccessory) ) + return new FHEMAccessory(log, connection, s); + + if( s.Attributes.disable == 1 ) { + that.log( s.Internals.NAME + ' is disabled'); + return null; + + } else if( s.Internals.TYPE == 'structure' ) { + that.log( s.Internals.NAME + ' is a structure'); + return null; + + } + + this.mappings = {}; var match; @@ -467,6 +486,9 @@ FHEMAccessory(log, connection, s) { if( s.Readings.humidity ) this.mappings.humidity = { reading: 'humidity' }; + if( s.Readings.voc ) + this.mappings.airquality = { reading: 'voc' }; + if( s.Readings.motor ) this.mappings.motor = { reading: 'motor' }; @@ -529,8 +551,11 @@ FHEMAccessory(log, connection, s) { if( s.Internals.TYPE == 'SONOSPLAYER' ) //FIXME: use sets [Pp]lay/[Pp]ause/[Ss]top this.mappings.onOff = { reading: 'transportState', cmdOn: 'play', cmdOff: 'pause' }; else if( s.PossibleSets.match(/[\^ ]on\b/) - && s.PossibleSets.match(/[\^ ]off\b/) ) + && s.PossibleSets.match(/[\^ ]off\b/) ) { this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; + if( !s.Readings.state ) + delete this.mappings.onOff.reading; + } var event_map = s.Attributes.eventMap; if( event_map ) { @@ -559,23 +584,25 @@ FHEMAccessory(log, connection, s) { else if( this.mappings.thermostat ) log( s.Internals.NAME + ' is thermostat ['+ this.mappings.thermostat.reading +']' ); else if( this.mappings.contact ) - log( s.Internals.NAME + ' is contactsensor [' + this.mappings.contact.reading +']' ); + log( s.Internals.NAME + ' is contact sensor [' + this.mappings.contact.reading +']' ); else if( this.mappings.occupancy ) - log( s.Internals.NAME + ' is occupancysensor' ); + log( s.Internals.NAME + ' is occupancy sensor' ); else if( this.mappings.rgb ) - log( s.Internals.NAME + ' has RGB [0-' + this.mappings.rgb.reading +']'); + log( s.Internals.NAME + ' has RGB [' + this.mappings.rgb.reading +']'); else if( this.mappings.pct ) log( s.Internals.NAME + ' is dimable ['+ this.mappings.pct.reading +']' ); else if( s.hasDim ) log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); else if( s.isLight ) log( s.Internals.NAME + ' is light' ); - else + else if( this.mappings.onOff ||Β s.isSwitch ) log( s.Internals.NAME + ' is switchable' ); + else if( !this.mappings ) + return {}; - if( this.hasOnOff ) - log( s.Internals.NAME + ' has OnOff [' + this.hasOnOff + ']' ); + if( this.mappings.onOff ) + log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff.reading + ';' + this.mappings.onOff.cmdOn +',' + this.mappings.onOff.cmdOff + ']' ); if( this.mappings.hue ) log( s.Internals.NAME + ' has hue [0-' + this.mappings.hue.max +']' ); if( this.mappings.sat ) @@ -584,17 +611,23 @@ FHEMAccessory(log, connection, s) { log( s.Internals.NAME + ' has temperature ['+ this.mappings.temperature.reading +']' ); if( this.mappings.humidity ) log( s.Internals.NAME + ' has humidity ['+ this.mappings.humidity.reading +']' ); + if( this.mappings.airquality ) + log( s.Internals.NAME + ' has voc ['+ this.mappings.airquality.reading +']' ); if( this.mappings.motor ) log( s.Internals.NAME + ' has motor' ); if( this.mappings.direction ) log( s.Internals.NAME + ' has direction' ); +//log( util.inspect(s) ); + // device info this.name = s.Internals.NAME; this.alias = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.device = s.Internals.NAME; this.type = s.Internals.TYPE; - this.model = s.Attributes.model ? s.Attributes.model : s.Internals.model; + this.model = s.Readings.model ? s.Readings.model.Value + : (s.Attributes.model ? s.Attributes.model + : ( s.Internals.model ? s.Internals.model : '' ) ); this.PossibleSets = s.PossibleSets; if( this.type == 'CUL_HM' ) { @@ -635,6 +668,7 @@ FHEMAccessory(log, connection, s) { if( value != undefined ) { var inform_id = that.device +'-'+ reading; that.mappings[key].informId = inform_id; + if( !that.mappings[key].nocache ) FHEM_cached[inform_id] = value; } @@ -649,6 +683,9 @@ FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', FHEMAccessory.prototype = { reading2homekit: function(reading,value) { + if( value == undefined ) + return undefined; + if( reading == 'hue' ) { value = Math.round(value * 360 / this.mappings.hue ? this.mappings.hue.max : 360); @@ -658,21 +695,21 @@ FHEMAccessory.prototype = { } else if( reading == 'pct' ) { value = parseInt( value ); - } else if(reading == 'direction') { - if( value.match(/^up/)) - value = 1; - else if( value.match(/^down/)) - value = 0; - else - value = 2; - } else if(reading == 'motor') { - if( value.match(/^opening/)) - value = 1; - else if( value.match(/^closing/)) - value = 0; + if( value.match(/^up/)) + value = Characteristic.PositionState.INCREASING; + else if( value.match(/^down/)) + value = Characteristic.PositionState.DECREASING; else - value = 2; + value = Characteristic.PositionState.STOPPED; + + } else if(reading == 'direction') { + if( value.match(/^opening/)) + value = PositionState.INCREASING; + else if( value.match(/^closing/)) + value = Characteristic.PositionState.DECREASING; + else + value = Characteristic.PositionState.STOPPED; } else if( reading == 'transportState' ) { if( value == 'PLAYING' ) @@ -686,24 +723,23 @@ FHEMAccessory.prototype = { } else if( reading == 'contact' ) { if( value.match( /^closed/ ) ) - value = 1; + value = Characteristic.ContactSensorState.CONTACT_DETECTED; else - value = 0; + value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'Window' ) { if( value.match( /^Closed/ ) ) - value = 1; + value = Characteristic.ContactSensorState.CONTACT_DETECTED; else - value = 0; + value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'lock' ) { - if( value.match( /^locked/ ) ) - value = 1; - else - value = 0; - if( value.match( /uncertain/ ) ) - value = 4; + value = Characteristic.LockCurrentState.UNKNOWN; + else if( value.match( /^locked/ ) ) + value = Characteristic.LockCurrentState.SECURED; + else + value = Characteristic.LockCurrentState.UNSECURED; } else if( reading == 'temperature' || reading == 'measured-temp' @@ -714,11 +750,24 @@ FHEMAccessory.prototype = { } else if( reading == 'humidity' ) { value = parseInt( value ); + } else if( reading == 'voc' ) { + value = parseInt( value ); + if( value > 1500 ) + Characteristic.AirQuality.POOR; + else if( value > 1000 ) + Characteristic.AirQuality.INFERIOR; + else if( value > 800 ) + Characteristic.AirQuality.FAIR; + else if( value > 600 ) + Characteristic.AirQuality.GOOD; + else if( value > 0 ) + Characteristic.AirQuality.EXCELLENT; + else + Characteristic.AirQuality.UNKNOWN; + } else if( reading == 'state' ) { if( value.match(/^set-/ ) ) return undefined; - if( value.match(/^set_/ ) ) - return undefined; if( this.event_map != undefined ) { var mapped = this.event_map[value]; @@ -728,11 +777,13 @@ FHEMAccessory.prototype = { if( value == 'off' ) value = 0; + else if( value == 'present' ) + value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; else if( value == 'absent' ) - value = 0; + value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; else if( value == '000000' ) value = 0; - else if( value.match( /^[A-D]0$/ ) ) // FIXME: should be handled by event_map now + else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now value = 0; else value = 1; @@ -760,7 +811,7 @@ FHEMAccessory.prototype = { if( this.type == 'HUEDevice' ) cmd = "set " + this.device + "alert select"; else - cmd = "set " + this.device + " toggle;; sleep 1;; set "+ this.device + " toggle"; + cmd = "set " + this.device + " toggle; sleep 1; set "+ this.device + " toggle"; } else if( c == 'set' ) { cmd = "set " + this.device + " " + value; @@ -864,14 +915,21 @@ FHEMAccessory.prototype = { }, query: function(reading, callback) { + if( reading == undefined ) { + if( callback != undefined ) + callback( 1 ); + return; + } + this.log("query: " + this.name + "-" + reading); var result = FHEM_cached[this.device + '-' + reading]; if( result != undefined ) { this.log(" cached: " + result); if( callback != undefined ) - callback( result ); - return( result ); + callback( undefined, result ); + return result; + } else this.log(" not cached" ); @@ -931,13 +989,12 @@ FHEMAccessory.prototype = { } else if( reading == 'lock' && query_reading == 'state') { - if( value.match( /^locked/ ) ) - value = 1; - else - value = 0; - if( value.match( /uncertain/ ) ) - value = 4; + value = Characteristic.LockCurrentState.UNKNOWN; + else if( value.match( /^locked/ ) ) + value = Characteristic.LockCurrentState.SECURED; + else + value = Characteristic.LockCurrentState.UNSECURED; } else if(reading == 'hue' && query_reading == that.mappings.rgb) { //FHEM_update( that.device+'-'+query_reading, value ); @@ -962,711 +1019,515 @@ FHEMAccessory.prototype = { that.log(" mapped: " + value); FHEM_update( that.device + '-' + reading, value, true ); - if( value == undefined ) - return; - if( callback != undefined ) - callback(value); - return(value); + if( callback != undefined ) { + if( value == undefined ) + callback(1); + else + callback(undefined, value); + } + + return value ; } ); }, - informationCharacteristics: function() { - return [ - { - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.alias, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "FHEM:"+this.type, - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model ? this.model : '', - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial ? this.serial : "", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: function(value) { - if( this.mappings.onOff ) - that.command( 'identify' ); - }, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }]; - }, + createDeviceService: function() { + var name = this.alias + 'xxx'; - controlCharacteristics: function(that) { - cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.alias, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }] - - if( this.mappings.onOff ) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); - }, - onUpdate: function(value) { - that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); - }, - onRead: function(callback) { - that.query( that.mappings.onOff.reading, function(state){ callback(state) } ); - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.onOff.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }); - } - - if( this.mappings.pct ) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.pct.informId, that); - }, - onUpdate: function(value) { that.command('pct', value); }, - onRead: function(callback) { - that.query(that.mappings.pct.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.pct.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } else if(Β this.hasDim ) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - // state is alreadi subscribed from POWER_STATE_CTYPE - FHEM_subscribe(characteristic, that.name+'-pct', that); - }, - onUpdate: function(value) { that.delayed('dim', value); }, - onRead: function(callback) { - that.query('pct', function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - //initialValue: FHEM_cached[that.mappings.dim.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: this.pctMax, - designedMinStep: 1, - unit: "%" - }); - } - - if( that.mappings.hue ) { - cTypes.push({ - cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.hue.informId, that); - }, - onUpdate: function(value) { that.command('hue', value); }, - onRead: function(callback) { - that.query(that.mappings.hue.reading, function(hue){ - callback(hue); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.hue.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Hue of the Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - } else if( this.mappings.rgb ) { - cTypes.push({ - cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-hue', that); - FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); - }, - onUpdate: function(value) { that.command('H-rgb', value); }, - onRead: function(callback) { - that.query('hue', function(hue){ - callback(hue); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Hue of the Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - - if( !this.mappings.sat ) - cTypes.push({ - cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-sat', that); - }, - onUpdate: function(value) { that.command('S-rgb', value); }, - onRead: function(callback) { - that.query('sat', function(sat){ - callback(sat); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Saturation of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - if( !this.mappings.pct ) - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-bri', that); - }, - onUpdate: function(value) { that.command('B-rgb', value); }, - onRead: function(callback) { - that.query('bri', function(bri){ - callback(bri); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: this.pctMax, - designedMinStep: 1, - unit: "%" - }); - } - - if( this.mappings.sat ) { - cTypes.push({ - cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.sat.informId, that); - }, - onUpdate: function(value) { that.command('sat', value); }, - onRead: function(callback) { - that.query(that.mappings.sat.reading, function(sat){ - callback(sat); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.sat.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Saturation of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - //FIXME: use mapping.volume - if( this.mappings.volume ) { - cTypes.push({ - cType: types.OUTPUTVOLUME_CTYPE, - onUpdate: function(value) { that.delayed('volume', value); }, - onRegister: function(characteristic) { - if( !that.mappings.volume.nocache ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.volume.informId, that); - } - }, - onRead: function(callback) { - that.query(that.mappings.volume.reading, function(volume){ - callback(volume); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 10, - //initialValue: FHEM_cached[that.mappings.volume.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Volume of this device", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1 - //unit: "%" - }); - } - - if( this.mappings.blind ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value) { that.delayed('targetPosition', value, 1500); }, - //onRegister: function(characteristic) { - // characteristic.eventEnabled = true; - // FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - //}, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Blind Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Blind Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.motor ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.motor.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.motor ) - that.query(that.mappings.motor.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if( this.mappings.window ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value) { that.delayed('targetPosition', value, 1500); }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(level){ - callback(level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 50, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-state', that); - FHEM_subscribe(characteristic, that.mappings.window.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.window.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.direction ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.direction.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.direction ) - that.query(that.mappings.direction.reading, function(direction){ - callback(direction); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if( this.mappings.garage ) { - cTypes.push({ - onUpdate: function(value) { - that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); - }, - cType: types.TARGET_DOORSTATE_CTYPE, - onRead: function(callback) { - callback(1); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target GarageDoor Position", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }); - cTypes.push({ - cType: types.CURRENT_DOOR_STATE_CTYPE, - onRead: function(callback) { - callback(4); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 4, - supportEvents: true, - supportBonjour: false, - manfDescription: "Current GarageDoor State", - designedMinValue: 0, - designedMaxValue: 4, - designedMinStep: 1, - designedMaxLength: 1 - }); - - cTypes.push({ - cType: types.OBSTRUCTION_DETECTED_CTYPE, - onRead: function(callback) { - callback(false); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Obstruction Detected", - designedMaxLength: 1 - }); - } - - //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep - if( this.mappings.thermostat ) { - cTypes.push({ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value) { that.delayed('targetTemperature', value, 1500); }, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.thermostat.reading, function(temperature){ - callback(temperature); - }); - }, - perms: ["pw","pr","ev"], - format: "float", - initialValue: FHEM_cached[that.mappings.thermostat.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: 5.0, - designedMaxValue: 30.0, - //designedMinStep: 0.5, - unit: "celsius" - }); - cTypes.push({ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - cTypes.push({ - cType: types.TARGETHEATINGCOOLING_CTYPE, - onUpdate: function(value) { that.command('targetMode', value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - }); - - cTypes.push({ - cType: types.TEMPERATURE_UNITS_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - }); - } - - if( this.mappings.contact ) { - cTypes.push({ - cType: types.CONTACT_SENSOR_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.contact.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.contact.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.contact.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Contact State", - designedMaxLength: 1 - }); - } - - if( this.mappings.occupancy ) { - cTypes.push({ - cType: types.OCCUPANCY_DETECTED_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.occupancy.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.occupancy.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Occupancy State", - designedMaxLength: 1 - }); - } - - if( this.mappings.temperature ) { - cTypes.push({ - cType: types.CURRENT_TEMPERATURE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.temperature.reading, function(temperature){ - callback(temperature); - }); - }, - perms: ["pr","ev"], - format: "float", - initialValue: FHEM_cached[that.mappings.temperature.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Temperature", - unit: "celsius" - }); - } - - if( this.mappings.humidity ) { - cTypes.push({ - cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.humidity.reading, function(humidity){ - callback(humidity); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.humidity.informId], - designedMinValue: 0, - designedMaxValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Humidity", - unit: "%" - }); - - } - - return cTypes; - }, - - sType: function() { - if( match = this.PossibleSets.match(/[\^ ]volume\b/) ) { - return types.SPEAKER_STYPE; - } else if( this.isSwitch ) { - return types.SWITCH_STYPE; + if( this.isSwitch ) { + this.log(" switch service for " + this.name) + return new Service.Switch(name); } else if( this.mappings.garage ) { - return types.GARAGE_DOOR_OPENER_STYPE; + this.log(" garage door opener service for " + this.name) + return new Service.GarageDoorOpener(name); } else if( this.mappings.window ) { - return types.WINDOW_STYPE; + this.log(" window service for " + this.name) + return new Service.Window(name); } else if( this.mappings.blind ) { - return types.WINDOW_COVERING_STYPE; + this.log(" window covering service for " + this.name) + return new Service.WindowCovering(name); } else if( this.mappings.thermostat ) { - return types.THERMOSTAT_STYPE; + this.log(" thermostat service for " + this.name) + return new Service.Thermostat(name); } else if( this.mappings.contact ) { - return types.CONTACT_SENSOR_STYPE; + this.log(" contact sensor service for " + this.name) + return new Service.ContactSensor(name); } else if( this.mappings.occupancy ) { - return types.OCCUPANCY_SENSOR_STYPE; + this.log(" occupancy sensor service for " + this.name) + return new Service.OccupancySensor(name); } else if( this.isLight || this.mappings.pct ||Β this.mappings.hue ||Β this.mappings.rgb ) { - return types.LIGHTBULB_STYPE; + this.log(" lightbulb service for " + this.name) + return new Service.Lightbulb(name); } else if( this.mappings.temperature ) { - return types.TEMPERATURE_SENSOR_STYPE; + this.log(" temperature sensor service for " + this.name) + return new Service.TemperatureSensor(name); } else if( this.mappings.humidity ) { - return types.HUMIDITY_SENSOR_STYPE; - } else { - return types.SWITCH_STYPE; + this.log(" humidity sensor service for " + this.name) + return new Service.HumiditySensor(name); + } else if( this.mappings.airquality ) { + this.log(" humidity sensor service for " + this.name) + return new Service.AirQualitySensor(name); } + + this.log(" switch service for " + this.name) + return new Service.Switch(name); + }, + + identify: function(callback) { + this.log('['+this.name+'] identify requested!'); + if( match = this.PossibleSets.match(/[\^ ]toggle\b/) ) { + this.command( 'identify' ); + } + callback(); }, getServices: function() { + this.log("creating services for " + this.name) + + this.log(" information service for " + this.name) + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "FHEM:"+this.type) + .setCharacteristic(Characteristic.Model, "FHEM:"+this.model ? this.model : '') + .setCharacteristic(Characteristic.SerialNumber, this.serial ? this.serial : ''); + + var controlService = this.createDeviceService(); + var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), - }, - { - sType: this.sType(), - characteristics: this.controlCharacteristics(that) - }]; - this.log("Loaded services for " + this.name) - return services; + if( this.mappings.onOff ) { + this.log(" power characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.On); + + FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); + if( FHEM_cached[that.mappings.onOff.informId] != undefined ) + characteristic.value = FHEM_cached[that.mappings.onOff.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.onOff.reading, callback); + }.bind(this) ); + } + + if( this.mappings.pct ) { + this.log(" brightness characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Brightness); + + FHEM_subscribe(characteristic, that.mappings.pct.informId, that); + if( FHEM_cached[that.mappings.pct.informId] != undefined ) + characteristic.value = FHEM_cached[that.mappings.pct.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('pct', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.pct.reading, callback); + }.bind(this) ); + + } else if(Β this.hasDim ) { + this.log(" fake brightness characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Brightness); + + FHEM_subscribe(characteristic, that.name+'-pct', that); + characteristic.value = 0; + characteristic.maximumValue = this.pctMax; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('dim', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('pct', callback); + }.bind(this) ); + + } + + if( that.mappings.hue ) { + this.log(" hue characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Hue); + + FHEM_subscribe(characteristic, that.mappings.hue.informId, that); + if( FHEM_cached[that.mappings.hue.informId] != undefined ) + characteristic.value = FHEM_cached[that.mappings.hue.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('hue', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.hue.reading, callback); + }.bind(this) ); + + } else if( this.mappings.rgb ) { + this.log(" fake hue characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Hue); + + FHEM_subscribe(characteristic, that.name+'-hue', that); + FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); + characteristic.value = 0; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('H-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('hue', callback); + }.bind(this) ); + + if( !this.mappings.sat ) { + this.log(" fake saturation characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Saturation); + + FHEM_subscribe(characteristic, that.name+'-sat', that); + characteristic.value = 100; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('S-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('sat', callback); + }.bind(this) ); + } + + if( !this.mappings.pct ) { + this.log(" fake brightness characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Brightness); + + FHEM_subscribe(characteristic, that.name+'-bri', that); + characteristic.value = 0; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('B-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('bri', callback); + }.bind(this) ); + } + + } + + if( this.mappings.sat ) { + this.log(" saturation characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Saturation); + + FHEM_subscribe(characteristic, that.mappings.sat.informId, that); + if( FHEM_cached[that.mappings.sat.informId] != undefined ) + characteristic.value = FHEM_cached[that.mappings.sat.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('sat', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.sat.reading, callback); + }.bind(this) ); + } + + if( this.mappings.volume ) { + this.log(" custom volume characteristic for " + this.name) + + var characteristic = new Characteristic('Volume', '00000027-0000-1000-8000-0026BB765291'); // FIXME!!! + controlService.addCharacteristic(characteristic); + + if( !that.mappings.volume.nocache ) { + FHEM_subscribe(characteristic, that.mappings.volume.informId, that); + characteristic.value = FHEM_cached[that.mappings.volume.informId]; + } else { + characteristic.value = 10; + } + + characteristic.format = 'uint8'; + characteristic.unit = 'percentage'; + characteristic.maximumValue = 100; + characteristic.minimumValue = 0; + characteristic.stepValue = 1; + characteristic.readable = true; + characteristic.writable = true; + characteristic.supportsEventNotification = true; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('volume', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.volume.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.blind ) { + this.log(" current position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); + + FHEM_subscribe(characteristic, that.mappings.blind.informId, that); + characteristic.value = FHEM_cached[that.mappings.blind.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.blind.reading, callback); + }.bind(this) ); + + + this.log(" target position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); + + characteristic.value = FHEM_cached[that.mappings.blind.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetPosition', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.blind.reading, callback); + }.bind(this) ); + + + this.log(" position state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.PositionState); + + if( that.mappings.motor ) + FHEM_subscribe(characteristic, that.mappings.motor.informId, that); + characteristic.value = that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:Characteristic.PositionState.STOPPED; + + characteristic + .on('get', function(callback) { + if( that.mappings.motor ) + that.query(that.mappings.motor.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.window ) { + this.log(" current position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); + + FHEM_subscribe(characteristic, that.name+'-state', that); + FHEM_subscribe(characteristic, that.mappings.window.informId, that); + characteristic.value = FHEM_cached[that.mappings.window.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.window.reading, callback); + }.bind(this) ); + + + this.log(" target position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); + + characteristic.value = FHEM_cached[that.mappings.window.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetPosition', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.window.reading, callback); + }.bind(this) ); + + + this.log(" position state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.PositionState); + + if( that.mappings.direction ) + FHEM_subscribe(characteristic, that.mappings.direction.informId, that); + characteristic.value = that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:Characteristic.PositionState.STOPPED; + + characteristic + .on('get', function(callback) { + if( that.mappings.direction ) + that.query(that.mappings.direction.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.garage ) { + this.log(" current door state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentDoorState); + + characteristic.value = Characteristic.CurrentDoorState.STOPPED; + + characteristic + .on('get', function(callback) { + callback(undefined, Characteristic.CurrentDoorState.STOPPED); + }.bind(this) ); + + + this.log(" target door state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetDoorState); + + characteristic.value = 1; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); + callback(); + }.bind(this) ) + .on('get', function(callback) { + callback(undefined,0); + }.bind(this) ); + + + this.log(" obstruction detected characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.ObstructionDetected); + + //FHEM_subscribe(characteristic, that.mappings.direction.informId, that); + characteristic.value = 0; + + characteristic + .on('get', function(callback) { + callback(undefined,1); + }.bind(this) ); + + } + + if( this.mappings.temperature ) { + this.log(" temperature characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentTemperature) + || controlService.addCharacteristic(Characteristic.CurrentTemperature); + + FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); + characteristic.value = FHEM_cached[that.mappings.temperature.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.temperature.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.humidity ) { + this.log(" humidity characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentRelativeHumidity) + || controlService.addCharacteristic(Characteristic.CurrentRelativeHumidity); + + FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); + characteristic.value = FHEM_cached[that.mappings.humidity.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.humidity.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.airquality ) { + this.log(" air quality characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.AirQuality) + || controlService.addCharacteristic(Characteristic.AirQuality); + + FHEM_subscribe(characteristic, that.mappings.airquality.informId, that); + characteristic.value = FHEM_cached[that.mappings.airquality.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.airquality.reading, callback); + }.bind(this) ); + } + + + //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep + if( this.mappings.thermostat ) { + this.log(" target temperature characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetTemperature); + + FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); + characteristic.value = FHEM_cached[that.mappings.thermostat.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetTemperature', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.thermostat.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.contact ) { + this.log(" contact sensor characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.ContactSensorState); + + FHEM_subscribe(characteristic, that.mappings.contact.informId, that); + characteristic.value = FHEM_cached[that.mappings.contact.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.contact.reading, callback); + }.bind(this) ); + + } + + if( this.mappings.occupancy ) { + this.log(" occupancy detected characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.OccupancyDetected); + + FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); + characteristic.value = FHEM_cached[that.mappings.occupancy.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.occupancy.reading, callback); + }.bind(this) ); + + } + + return [informationService, controlService]; } + }; //module.exports.accessory = FHEMAccessory; @@ -1677,47 +1538,23 @@ module.exports.platform = FHEMPlatform; //http server for debugging var http = require('http'); -const FHEMdebug_PORT=8080; +const FHEMdebug_PORT=8081; function FHEMdebug_handleRequest(request, response){ //console.log( request ); if( request.url == "/cached" ) { response.write( "home

" ); - if( FHEM_lastEventTimestamp ) - response.write( "FHEM_lastEventTime: "+ new Date(FHEM_lastEventTimestamp) +"

" ); + if( FHEM_lastEventTime ) + response.write( "FHEM_lastEventTime: "+ new Date(FHEM_lastEventTime) +"

" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); } else if( request.url == "/subscriptions" ) { response.write( "home

" ); response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); - } else if( request.url == "/persist" ) { - response.write( "home

" ); - var unique = {}; - Object.keys(FHEM_subscriptions).forEach(function(key) { - var characteristic = FHEM_subscriptions[key].characteristic; - var info = characteristic.accessoryController.tcpServer.accessoryInfo; - if( unique[info.displayName] ) - return; - unique[info.displayName] = info.username; - - var accessory = FHEM_subscriptions[key].accessory; - - //var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - //accessory.execute( cmd ); - } ); - - var keys = Object.keys(unique); - keys.sort(); - for( i = 0; i < keys.length; i++ ) { - var k = keys[i]; - response.write( k +': '+ unique[k] +'
' ); - } - response.end( "" ); - } else - response.end( "cached
persist
subscriptions" ); + response.end( "cached
subscriptions" ); } var FHEMdebug_server = http.createServer( FHEMdebug_handleRequest ); diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js new file mode 100644 index 0000000..5a23ab5 --- /dev/null +++ b/platforms/HomeAssistant.js @@ -0,0 +1,533 @@ +// Home Assistant +// +// Current Support: lights +// +// This is a shim to publish lights maintained by Home Assistant. +// Home Assistant is an open-source home automation platform. +// URL: http://home-assistant.io +// GitHub: https://github.com/balloob/home-assistant +// +// HA accessories supported: Lights, Switches, Media Players, Scenes. +// +// Optional Devices - Edit the supported_types key in the config to pick which +// of the 4 types you would like to expose to HomeKit from +// Home Assistant. light, switch, media_player, scene. +// +// +// Scene Support +// +// You can optionally import your Home Assistant scenes. These will appear to +// HomeKit as switches. You can simply say "turn on party time". In some cases +// scenes names are already rerved in HomeKit...like "Good Morning" and +// "Good Night". You will be able to just say "Good Morning" or "Good Night" to +// have these triggered. +// +// You might want to play with the wording to figure out what ends up working well +// for your scene names. It's also important to not populate any actual HomeKit +// scenes with the same names, as Siri will pick these instead of your Home +// Assistant scenes. +// +// +// +// Media Player Support +// +// Media players on your Home Assistant will be added to your HomeKit as a light. +// While this seems like a hack at first, it's actually quite useful. You can +// turn them on, off, and set their volume (as a function of brightness). +// +// There are some rules to know about how on/off treats your media player. If +// your media player supports play/pause, then turning them on and off via +// HomeKit will play and pause them. If they do not support play/pause but then +// support on/off they will be turned on and then off. +// +// HomeKit does not have a characteristic of Volume or a Speaker type. So we are +// using the light device type here. So to turn your speaker up and down, you +// will need to use the same language you use to set the brighness of a light. +// You can play around with language to see what fits best. +// +// +// +// Examples +// +// Dim the Kitchen Speaker to 40% - sets volume to 40% +// Dim the the Kitchen Speaker 10% - lowers the volume by 10% +// Set the brightness of the Kitchen Speaker to 40% +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "HomeAssistant", +// "name": "HomeAssistant", +// "host": "http://192.168.1.50:8123", +// "password": "xxx", +// "supported_types": ["light", "switch", "media_player", "scene"] +// } +// ] +// +// When you attempt to add a device, it will ask for a "PIN code". +// The default code for all HomeBridge accessories is 031-45-154. + +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var url = require('url') +var request = require("request"); + +var communicationError = new Error('Can not communicate with Home Assistant.') + +function HomeAssistantPlatform(log, config){ + + // auth info + this.host = config["host"]; + this.password = config["password"]; + this.supportedTypes = config["supported_types"]; + + this.log = log; +} + +HomeAssistantPlatform.prototype = { + _request: function(method, path, options, callback) { + var self = this + var requestURL = this.host + '/api' + path + options = options || {} + options.query = options.query || {} + + var reqOpts = { + url: url.parse(requestURL), + method: method || 'GET', + qs: options.query, + body: JSON.stringify(options.body), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-ha-access': this.password + } + } + + request(reqOpts, function onResponse(error, response, body) { + if (error) { + callback(error, response) + return + } + + if (response.statusCode === 401) { + callback(new Error('You are not authenticated'), response) + return + } + + json = JSON.parse(body) + callback(error, response, json) + }) + + }, + fetchState: function(entity_id, callback){ + this._request('GET', '/states/' + entity_id, {}, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + callService: function(domain, service, service_data, callback){ + var options = {} + options.body = service_data + + this._request('POST', '/services/' + domain + '/' + service, options, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + accessories: function(callback) { + this.log("Fetching HomeAssistant devices."); + + var that = this; + var foundAccessories = []; + + this._request('GET', '/states', {}, function(error, response, data){ + + for (var i = 0; i < data.length; i++) { + entity = data[i] + entity_type = entity.entity_id.split('.')[0] + + if (that.supportedTypes.indexOf(entity_type) == -1) { + continue; + } + + var accessory = null + + if (entity_type == 'light') { + accessory = new HomeAssistantLight(that.log, entity, that) + }else if (entity_type == 'switch'){ + accessory = new HomeAssistantSwitch(that.log, entity, that) + }else if (entity_type == 'scene'){ + accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene') + }else if (entity_type == 'media_player' && entity.attributes && entity.attributes.supported_media_commands){ + accessory = new HomeAssistantMediaPlayer(that.log, entity, that) + } + + if (accessory) { + foundAccessories.push(accessory) + } + } + + callback(foundAccessories) + }) + + } +} + +function HomeAssistantLight(log, data, client) { + // device info + this.domain = "light" + this.data = data + this.entity_id = data.entity_id + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } + + this.client = client + this.log = log; +} + +HomeAssistantLight.prototype = { + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data) { + powerState = data.state == 'on' + callback(null, powerState) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getBrightness: function(callback){ + this.log("fetching brightness for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data && data.attributes) { + brightness = ((data.attributes.brightness || 0) / 255)*100 + callback(null, brightness) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + setPowerState: function(powerOn, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this.client.callService(this.domain, 'turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this.client.callService(this.domain, 'turn_off', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + } + }, + setBrightness: function(level, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + service_data.brightness = 255*(level/100.0) + + this.log("Setting brightness on the '"+this.name+"' to " + level); + + this.client.callService(this.domain, 'turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set brightness on the '"+that.name+"' to " + level); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getServices: function() { + var lightbulbService = new Service.Lightbulb(); + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, "Light") + .setCharacteristic(Characteristic.SerialNumber, "xxx"); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + lightbulbService + .addCharacteristic(Characteristic.Brightness) + .on('get', this.getBrightness.bind(this)) + .on('set', this.setBrightness.bind(this)); + + return [informationService, lightbulbService]; + } + +} + +function HomeAssistantMediaPlayer(log, data, client) { + var SUPPORT_PAUSE = 1 + var SUPPORT_SEEK = 2 + var SUPPORT_VOLUME_SET = 4 + var SUPPORT_VOLUME_MUTE = 8 + var SUPPORT_PREVIOUS_TRACK = 16 + var SUPPORT_NEXT_TRACK = 32 + var SUPPORT_YOUTUBE = 64 + var SUPPORT_TURN_ON = 128 + var SUPPORT_TURN_OFF = 256 + + // device info + this.domain = "media_player" + this.data = data + this.entity_id = data.entity_id + this.supportsVolume = false + this.supportedMediaCommands = data.attributes.supported_media_commands + + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } + + if ((this.supportedMediaCommands | SUPPORT_PAUSE) == this.supportedMediaCommands) { + this.onState = "playing" + this.offState = "paused" + this.onService = "media_play" + this.offService = "media_pause" + }else if ((this.supportedMediaCommands | SUPPORT_TURN_ON) == this.supportedMediaCommands && (this.supportedMediaCommands | SUPPORT_TURN_OFF) == this.supportedMediaCommands) { + this.onState = "on" + this.offState = "off" + this.onService = "turn_on" + this.offService = "turn_off" + } + + if ((this.supportedMediaCommands | SUPPORT_VOLUME_SET) == this.supportedMediaCommands) { + this.supportsVolume = true + } + + this.client = client + this.log = log; +} + +HomeAssistantMediaPlayer.prototype = { + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data) { + powerState = data.state == this.onState + callback(null, powerState) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getVolume: function(callback){ + this.log("fetching volume for: " + this.name); + that = this + this.client.fetchState(this.entity_id, function(data){ + if (data && data.attributes) { + that.log(JSON.stringify(data.attributes)) + level = data.attributes.volume_level ? data.attributes.volume_level*100 : 0 + callback(null, level) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + setPowerState: function(powerOn, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this.client.callService(this.domain, this.onService, service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this.client.callService(this.domain, this.offService, service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + } + }, + setVolume: function(level, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + service_data.volume_level = level/100.0 + + this.log("Setting volume on the '"+this.name+"' to " + level); + + this.client.callService(this.domain, 'volume_set', service_data, function(data){ + if (data) { + that.log("Successfully set volume on the '"+that.name+"' to " + level); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getServices: function() { + var lightbulbService = new Service.Lightbulb(); + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, "Media Player") + .setCharacteristic(Characteristic.SerialNumber, "xxx"); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + + if (this.supportsVolume) { + lightbulbService + .addCharacteristic(Characteristic.Brightness) + .on('get', this.getVolume.bind(this)) + .on('set', this.setVolume.bind(this)); + } + + return [informationService, lightbulbService]; + } + +} + + +function HomeAssistantSwitch(log, data, client, type) { + // device info + this.domain = type || "switch" + this.data = data + this.entity_id = data.entity_id + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } + + this.client = client + this.log = log; +} + +HomeAssistantSwitch.prototype = { + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data) { + powerState = data.state == 'on' + callback(null, powerState) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + setPowerState: function(powerOn, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this.client.callService(this.domain, 'turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this.client.callService(this.domain, 'turn_off', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + } + }, + getServices: function() { + var switchService = new Service.Switch(); + var informationService = new Service.AccessoryInformation(); + var model; + + switch (this.domain) { + case "scene": + model = "Scene" + break; + default: + model = "Switch" + } + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, model) + .setCharacteristic(Characteristic.SerialNumber, "xxx"); + + if (this.domain == 'switch') { + switchService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + }else{ + switchService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + } + + return [informationService, switchService]; + } + +} + +module.exports.accessory = HomeAssistantLight; +module.exports.accessory = HomeAssistantMediaPlayer; +module.exports.accessory = HomeAssistantSwitch; +module.exports.platform = HomeAssistantPlatform; diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js new file mode 100644 index 0000000..5b79c8e --- /dev/null +++ b/platforms/ZWayServer.js @@ -0,0 +1,672 @@ +var debug = require('debug')('ZWayServer'); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var types = require("HAP-NodeJS/accessories/types.js"); +var request = require("request"); +var tough = require('tough-cookie'); +var Q = require("q"); + +function ZWayServerPlatform(log, config){ + this.log = log; + this.url = config["url"]; + this.login = config["login"]; + this.password = config["password"]; + this.name_overrides = config["name_overrides"]; + this.batteryLow = config["battery_low_level"] || 15; + this.pollInterval = config["poll_interval"] || 2; + this.splitServices= config["split_services"] || false; + this.lastUpdate = 0; + this.cxVDevMap = {}; + this.vDevStore = {}; + this.sessionId = ""; + this.jar = request.jar(new tough.CookieJar()); +} + +ZWayServerPlatform.getVDevTypeKey = function(vdev){ + return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "") +} + +ZWayServerPlatform.prototype = { + + zwayRequest: function(opts){ + var that = this; + var deferred = Q.defer(); + + opts.jar = true;//this.jar; + opts.json = true; + opts.headers = { + "Cookie": "ZWAYSession=" + this.sessionId + }; + + request(opts, function(error, response, body){ + if(response.statusCode == 401){ + debug("Authenticating..."); + request({ + method: "POST", + url: that.url + 'ZAutomation/api/v1/login', + body: { //JSON.stringify({ + "form": true, + "login": that.login, + "password": that.password, + "keepme": false, + "default_ui": 1 + }, + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + json: true, + jar: true//that.jar + }, function(error, response, body){ + if(response.statusCode == 200){ + that.sessionId = body.data.sid; + opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId; + debug("Authenticated. Resubmitting original request..."); + request(opts, function(error, response, body){ + if(response.statusCode == 200){ + deferred.resolve(body); + } else { + deferred.reject(response); + } + }); + } else { + deferred.reject(response); + } + }); + } else if(response.statusCode == 200) { + deferred.resolve(body); + } else { + deferred.reject(response); + } + }); + return deferred.promise; + } + , + + accessories: function(callback) { + debug("Fetching Z-Way devices..."); + + //TODO: Unify this with getVDevServices, so there's only one place with mapping between service and vDev type. + //Note: Order matters! + var primaryDeviceClasses = [ + "switchBinary", + "thermostat", + "sensorBinary.Door/Window", + "sensorMultilevel.Temperature", + "switchMultilevel" + ]; + + var that = this; + var foundAccessories = []; + + this.zwayRequest({ + method: "GET", + url: this.url + 'ZAutomation/api/v1/devices' + }).then(function(result){ + this.lastUpdate = result.data.updateTime; + + var devices = result.data.devices; + var groupedDevices = {}; + for(var i = 0; i < devices.length; i++){ + var vdev = devices[i]; + if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } + var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); + gd.devices.push(vdev); + gd.types[ZWayServerPlatform.getVDevTypeKey(vdev)] = gd.devices.length - 1; + gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility + } + //TODO: Make a second pass, re-splitting any devices that don't make sense together + for(var gdid in groupedDevices) { + if(!groupedDevices.hasOwnProperty(gdid)) continue; + + // Debug/log... + debug('Got grouped device ' + gdid + ' consiting of devices:'); + var gd = groupedDevices[gdid]; + for(var j = 0; j < gd.devices.length; j++){ + debug(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); + } + + var accessory = null; + for(var ti = 0; ti < primaryDeviceClasses.length; ti++){ + if(gd.types[primaryDeviceClasses[ti]] !== undefined){ + gd.primary = gd.types[primaryDeviceClasses[ti]]; + var pd = gd.devices[gd.primary]; + var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; + debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary."); + accessory = new ZWayServerAccessory(name, gd, that); + break; + } + } + + if(!accessory) + debug("WARN: Didn't find suitable device class!"); + else + foundAccessories.push(accessory); + + } +//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... + callback(foundAccessories); + + // Start the polling process... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + + }.bind(this)); + + } + , + + pollUpdate: function(){ + //debug("Polling for updates since " + this.lastUpdate + "..."); + return this.zwayRequest({ + method: "GET", + url: this.url + 'ZAutomation/api/v1/devices', + qs: {since: this.lastUpdate} + }).then(function(result){ + this.lastUpdate = result.data.updateTime; + if(result.data && result.data.devices && result.data.devices.length){ + var updates = result.data.devices; + debug("Got " + updates.length + " updates."); + for(var i = 0; i < updates.length; i++){ + var upd = updates[i]; + if(this.cxVDevMap[upd.id]){ + var vdev = this.vDevStore[upd.id]; + vdev.metrics.level = upd.metrics.level; + vdev.updateTime = upd.updateTime; + var cxs = this.cxVDevMap[upd.id]; + for(var j = 0; j < cxs.length; j++){ + var cx = cxs[j]; + if(typeof cx.zway_getValueFromVDev !== "function") continue; + var oldValue = cx.value; + var newValue = cx.zway_getValueFromVDev(vdev); + if(oldValue !== newValue){ + cx.value = newValue; + cx.emit('change', { oldValue:oldValue, newValue:cx.value, context:null }); + debug("Updated characteristic " + cx.displayName + " on " + vdev.metrics.title); + } + } + } + } + } + + // setup next poll... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + }.bind(this)); + } + +} + +function ZWayServerAccessory(name, devDesc, platform) { + // device info + this.name = name; + this.devDesc = devDesc; + this.platform = platform; + this.log = platform.log; +} + + +ZWayServerAccessory.prototype = { + + getVDev: function(vdev){ + return this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + })//.then(function()); + } + , + command: function(vdev, command, value) { + return this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command, + qs: (value === undefined ? undefined : value) + }); + }, + + getVDevServices: function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + var services = [], service; + switch (typeKey) { + case "switchBinary": + services.push(new Service.Switch(vdev.metrics.title)); + break; + case "switchMultilevel": + services.push(new Service.Lightbulb(vdev.metrics.title)); + break; + case "thermostat": + services.push(new Service.Thermostat(vdev.metrics.title)); + break; + case "sensorMultilevel.Temperature": + services.push(new Service.TemperatureSensor(vdev.metrics.title)); + break; + case "sensorBinary.Door/Window": + services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + break; + case "battery.Battery": + services.push(new Service.BatteryService(vdev.metrics.title)); + break; + case "sensorMultilevel.Luminiscence": + services.push(new Service.LightSensor(vdev.metrics.title)); + break; + } + + var validServices =[]; + for(var i = 0; i < services.length; i++){ + if(this.configureService(services[i], vdev)) + validServices.push(services[i]); + } + + return validServices; + } + , + uuidToTypeKeyMap: null + , + extraCharacteristicsMap: { + "battery.Battery": [Characteristic.BatteryLevel, Characteristic.StatusLowBattery], + "sensorMultilevel.Temperature": [Characteristic.CurrentTemperature, Characteristic.TemperatureDisplayUnits], + "sensorMultilevel.Luminiscence": [Characteristic.CurrentAmbientLightLevel] + } + , + getVDevForCharacteristic: function(cx, vdevPreferred){ + var map = this.uuidToTypeKeyMap; + if(!map){ + this.uuidToTypeKeyMap = map = {}; + map[(new Characteristic.On).UUID] = ["switchBinary","switchMultilevel"]; + map[(new Characteristic.Brightness).UUID] = ["switchMultilevel"]; + map[(new Characteristic.CurrentTemperature).UUID] = ["sensorMultilevel.Temperature","thermostat"]; + map[(new Characteristic.TargetTemperature).UUID] = ["thermostat"]; + map[(new Characteristic.TemperatureDisplayUnits).UUID] = ["sensorMultilevel.Temperature","thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.TargetHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; + map[(new Characteristic.TargetDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.ObstructionDetected).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"]; + map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"]; + map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentAmbientLightLevel).UUID] = ["sensorMultilevel.Luminiscence"]; + } + + if(cx instanceof Characteristic.Name) return vdevPreferred; + + // Special case!: If cx is a CurrentTemperature, ignore the preferred device...we want the sensor if available! + if(cx instanceof Characteristic.CurrentTemperature) vdevPreferred = null; + // + + var typekeys = map[cx.UUID]; + if(typekeys === undefined) return null; + + if(vdevPreferred && typekeys.indexOf(ZWayServerPlatform.getVDevTypeKey(vdevPreferred)) >= 0){ + return vdevPreferred; + } + + var candidates = this.devDesc.devices; + for(var i = 0; i < typekeys.length; i++){ + for(var j = 0; j < candidates.length; j++){ + if(ZWayServerPlatform.getVDevTypeKey(candidates[j]) === typekeys[i]) return candidates[j]; + } + } + + return null; + } + , + configureCharacteristic: function(cx, vdev){ + var that = this; + + // Add this combination to the maps... + if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; + this.platform.cxVDevMap[vdev.id].push(cx); + if(!this.platform.vDevStore[vdev.id]) this.platform.vDevStore[vdev.id] = vdev; + + if(cx instanceof Characteristic.Name){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.title; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, that.name); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.On){ + cx.zway_getValueFromVDev = function(vdev){ + var val = false; + if(vdev.metrics.level === "on"){ + val = true; + } else if(vdev.metrics.level <= 5) { + val = false; + } else if (vdev.metrics.level > 5) { + val = true; + } + return val; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('set', function(powerOn, callback){ + this.command(vdev, powerOn ? "on" : "off").then(function(result){ + callback(); + }); + }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); + return cx; + } + + if(cx instanceof Characteristic.Brightness){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + callback(); + }); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.CurrentTemperature){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 999; + return cx; + } + + if(cx instanceof Characteristic.TargetTemperature){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + //debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + callback(); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40; + return cx; + } + + if(cx instanceof Characteristic.TemperatureDisplayUnits){ + //TODO: Always in Β°C for now. + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TemperatureDisplayUnits.CELSIUS; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.CurrentHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.CurrentHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); + }); + return cx; + } + + if(cx instanceof Characteristic.TargetHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetHeatingCoolingState.HEAT); + }); + // Hmm... apparently if this is not setable, we can't add a thermostat change to a scene. So, make it writable but a no-op. + cx.writable = true; + cx.on('set', function(newValue, callback){ + debug("WARN: Set of TargetHeatingCoolingState not yet implemented, resetting to HEAT!") + callback(undefined, Characteristic.TargetHeatingCoolingState.HEAT); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.CurrentDoorState){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level === "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); + } + + if(cx instanceof Characteristic.TargetDoorState){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetDoorState.CLOSED; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetDoorState.CLOSED); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.ObstructionDetected){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.zway_getValueFromVDev = function(vdev){ + return false; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, false); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.BatteryLevel){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.StatusLowBattery){ + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.ChargingState){ + //TODO: No known chargeable devices(?), so always return false. + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.ChargingState.NOT_CHARGING; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.ChargingState.NOT_CHARGING); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.CurrentAmbientLightLevel){ + cx.zway_getValueFromVDev = function(vdev){ + if(vdev.metrics.scaleTitle === "%"){ + // Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values. + // This will probably change! + var lux = 0.0005 * (vdev.metrics.level^3.6); + if(lux < cx.minimumValue) return cx.minimumValue; + if(lux > cx.maximumValue) return cx.maximumValue; + return lux; + } else { + return vdev.metrics.level; + } + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); + return cx; + } + } + , + configureService: function(service, vdev){ + var success = true; + for(var i = 0; i < service.characteristics.length; i++){ + var cx = service.characteristics[i]; + var vdev = this.getVDevForCharacteristic(cx, vdev); + if(!vdev){ + success = false; + debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); + } + cx = this.configureCharacteristic(cx, vdev); + } + for(var i = 0; i < service.optionalCharacteristics.length; i++){ + var cx = service.optionalCharacteristics[i]; + var vdev = this.getVDevForCharacteristic(cx); + if(!vdev) continue; + cx = this.configureCharacteristic(cx, vdev); + if(cx) service.addCharacteristic(cx); + } + return success; + } + , + getServices: function() { + var that = this; + + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Z-Wave.me") + .setCharacteristic(Characteristic.Model, "Virtual Device (VDev version 1)") + .setCharacteristic(Characteristic.SerialNumber, "VDev-" + this.devDesc.devices[this.devDesc.primary].h) //FIXME: Is this valid?); + + var services = [informationService]; + + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); + + if(this.platform.splitServices){ + if(this.devDesc.types["battery.Battery"]){ + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + } + + // Odds and ends...if there are sensors that haven't been used, add services for them... + + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; + if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ + services = services.concat(this.getVDevServices(tempSensor)); + } + + var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; + if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ + services = services.concat(this.getVDevServices(lightSensor)); + } + } else { + // Everything outside the primary service gets added as optional characteristics... + var service = services[1]; + var existingCxUUIDs = {}; + for(var i = 0; i < service.characteristics.length; i++) existingCxUUIDs[service.characteristics[i].UUID] = true; + + for(var i = 0; i < this.devDesc.devices.length; i++){ + var vdev = this.devDesc.devices[i]; + if(this.platform.cxVDevMap[vdev.id]) continue; // Don't double-use anything + var extraCxClasses = this.extraCharacteristicsMap[ZWayServerPlatform.getVDevTypeKey(vdev)]; + var extraCxs = []; + if(!extraCxClasses || extraCxClasses.length === 0) continue; + for(var j = 0; j < extraCxClasses.length; j++){ + var cx = new extraCxClasses[j](); + if(existingCxUUIDs[cx.UUID]) continue; // Don't have two of the same Characteristic type in one service! + var vdev2 = this.getVDevForCharacteristic(cx, vdev); // Just in case...will probably return vdev. + if(!vdev2){ + // Uh oh... one of the extraCxClasses can't be configured! Abort all extras for this vdev! + extraCxs = []; // to wipe out any already setup cxs. + break; + } + this.configureCharacteristic(cx, vdev2); + extraCxs.push(cx); + } + for(var j = 0; j < extraCxs.length; j++) + service.addCharacteristic(extraCxs[j]); + } + } + + debug("Loaded services for " + this.name); + return services; + } +}; + +module.exports.accessory = ZWayServerAccessory; +module.exports.platform = ZWayServerPlatform; diff --git a/config-sample-knx.json b/platforms/config-sample-knx.json similarity index 82% rename from config-sample-knx.json rename to platforms/config-sample-knx.json index a8e52b1..d74a14f 100644 --- a/config-sample-knx.json +++ b/platforms/config-sample-knx.json @@ -7,6 +7,8 @@ }, "description": "This is an example configuration file for KNX platform shim", "hint": "Always paste into jsonlint.com validation page before starting your homebridge, saves a lot of frustration", + "hint2":"Replace all group addresses by current addresses of your installation, these are arbitrary examples!", + "hint3":"For valid services and their characteristics have a look at the knxdevice.md file in folder accessories!" "platforms": [ { "platform": "KNX", @@ -16,7 +18,7 @@ "accessories": [ { "accessory_type": "knxdevice", - "description": "Only generic type knxdevice is supported, all previous knx type have been merged into that.", + "description": "Only generic type knxdevice is supported, all previous knx types have been merged into that.", "name": "Living Room North Lamp", "services": [ { @@ -101,19 +103,32 @@ "description": "iOS9 Window covering (blinds etc) type, still WIP", "name": "Blinds", "Target": { - "Set": "address", - "Listen": "adresses" + "Set": "1/2/3", + "Listen": "1/2/4" }, "Current": { - "Set": "address", - "Listen": "adresses" + "Set": "1/3/1", + "Listen": "1/3/2" }, "PositionState": { - "Listen": "adresses" + "Listen": "2/7/1" } } ] - } + },{ + "accessory_type": "knxdevice", + + "description":"sample contact sensor device", + "name": "Office", + "services": [ + { + "type": "ContactSensor", + "name": "Office Door", + "ContactSensorState": { + "Listen": "5/3/5" + } + }, + ] } ],