From 45ae56cf12a5a3e5abaa0f48f0360e384577929d Mon Sep 17 00:00:00 2001 From: jmtatsch Date: Sun, 13 Sep 2015 14:53:35 +0200 Subject: [PATCH 01/23] Fix 406 error for lifxjs by git+https --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05db017..875ea30 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "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", From b1d0ef57ac5653db8560e52d2a1c2ad28da10d02 Mon Sep 17 00:00:00 2001 From: Alessandro Zummo Date: Mon, 14 Sep 2015 01:15:56 +0200 Subject: [PATCH 02/23] add mpd client accessory --- accessories/mpdclient.js | 89 ++++++++++++++++++++++++++++++++++++++++ config-sample.json | 7 ++++ package.json | 1 + 3 files changed, 97 insertions(+) create mode 100644 accessories/mpdclient.js 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/config-sample.json b/config-sample.json index afb2893..6286591 100644 --- a/config-sample.json +++ b/config-sample.json @@ -183,6 +183,13 @@ "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" } ] } diff --git a/package.json b/package.json index 05db017..1233014 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "wink-js": "0.0.5", "xml2js": "0.4.x", "xmldoc": "0.1.x", + "komponist" : "0.1.0", "yamaha-nodejs": "0.4.x", "debug": "^2.2.0" } From f5cc6cf6fb8128736506b8749ef69e66dd96f3d5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:24:39 -0400 Subject: [PATCH 03/23] add home assistant shim --- platforms/HomeAssistant.js | 301 +++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 platforms/HomeAssistant.js diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js new file mode 100644 index 0000000..f2b5cd1 --- /dev/null +++ b/platforms/HomeAssistant.js @@ -0,0 +1,301 @@ +// 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 +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "HomeAssistant", +// "name": "HomeAssistant", +// "host": "http://192.168.1.50:8123", +// "password": "xxx" +// } +// ] +// +// 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 types = require("HAP-NodeJS/accessories/types.js"); +var url = require('url') +var request = require("request"); + +function HomeAssistantPlatform(log, config){ + + // auth info + this.host = config["host"]; + this.password = config["password"]; + + 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) + }) + + }, + accessories: function(callback) { + this.log("Fetching HomeAssistant devices."); + + var that = this; + var foundAccessories = []; + var lightsRE = /^light\./i + + + this._request('GET', '/states', {}, function(error, response, data){ + + for (var i = 0; i < data.length; i++) { + entity = data[i] + + if (entity.entity_id.match(lightsRE)) { + accessory = new HomeAssistantAccessory(that.log, entity, that) + foundAccessories.push(accessory) + } + } + + callback(foundAccessories) + }) + + } +} + +function HomeAssistantAccessory(log, data, client) { + // device info + this.data = data + this.entity_id = data.entity_id + this.name = data.attributes.friendly_name + + this.client = client + this.log = log; +} + +HomeAssistantAccessory.prototype = { + _callService: function(service, service_data, callback){ + var options = {} + options.body = service_data + + this.client._request('POST', '/services/light/' + service, options, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + _fetchState: function(callback){ + this.client._request('GET', '/states/' + this.entity_id, {}, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + this._fetchState(function(data){ + if (data) { + powerState = data.state == 'on' + callback(powerState) + }else{ + callback(null) + } + }) + }, + getBrightness: function(callback){ + this.log("fetching brightness for: " + this.name); + that = this + this._fetchState(function(data){ + if (data && data.attributes) { + that.log('returned brightness: ' + data.attributes.brightness) + brightness = ((data.attributes.brightness || 0) / 255)*100 + callback(brightness) + }else{ + callback(null) + } + }) + }, + setPowerState: function(powerOn) { + 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._callService('turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + } + }) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this._callService('turn_off', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + } + }) + } + }, + setBrightness: function(level) { + 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._callService('turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set brightness on the '"+that.name+"' to " + level); + } + }) + }, + 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: "HomeAssistant", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Rev-1", + 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.LIGHTBULB_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + },{ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + that.setPowerState(value); + }, + onRead: function(callback) { + that.getPowerState(function(powerState){ + callback(powerState); + }); + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state of the Bulb", + designedMaxLength: 1 + },{ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { + that.setBrightness(value); + }, + onRead: function(callback) { + that.getBrightness(function(level){ + callback(level); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 255, + designedMinStep: 1, + unit: "%" + }] + }]; + } + +} + +module.exports.accessory = HomeAssistantAccessory; +module.exports.platform = HomeAssistantPlatform; From ec550d1638d17326723aa687e7551b1010596396 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:24:54 -0400 Subject: [PATCH 04/23] add sample config for home assistant --- config-sample.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config-sample.json b/config-sample.json index afb2893..58a1f4b 100644 --- a/config-sample.json +++ b/config-sample.json @@ -89,6 +89,12 @@ "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" } ], From a6d61cc93afab8b86cde6c60534881e7f450c9dc Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:30:47 -0400 Subject: [PATCH 05/23] add link to HA in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From b39b33726d2ad1fb6ad6459b7c7d1e2013b4f41b Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 14 Sep 2015 05:43:11 +0200 Subject: [PATCH 06/23] Make TargetHeatingCoolingState writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently, if TargetHeatingCoolingState is not writable, you can’t add a thermostat to a scene. This fix makes it writable from HomeKit, but it still always remains set to HEAT. --- platforms/ZWayServer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 322b9fb..5b79c8e 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -458,7 +458,12 @@ ZWayServerAccessory.prototype = { debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); }); - cx.writable = false; + // 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; } From 167a983068000b9f26cab30f726285777fe04b1f Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 00:14:02 -0400 Subject: [PATCH 07/23] handle missing friendly name --- platforms/HomeAssistant.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index f2b5cd1..ce7fc3d 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -97,7 +97,11 @@ function HomeAssistantAccessory(log, data, client) { // device info this.data = data this.entity_id = data.entity_id - this.name = data.attributes.friendly_name + 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; From 544124fbabdc803ab5e245f4d2c8a52fca2a1c94 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 00:20:28 -0400 Subject: [PATCH 08/23] clean it up and get on that modern tip --- platforms/HomeAssistant.js | 116 ++++--------------------------------- 1 file changed, 11 insertions(+), 105 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index ce7fc3d..727cc77 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -192,111 +192,17 @@ HomeAssistantAccessory.prototype = { }) }, 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: "HomeAssistant", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-1", - 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.LIGHTBULB_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - that.setPowerState(value); - }, - onRead: function(callback) { - that.getPowerState(function(powerState){ - callback(powerState); - }); - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state of the Bulb", - designedMaxLength: 1 - },{ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { - that.setBrightness(value); - }, - onRead: function(callback) { - that.getBrightness(function(level){ - callback(level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 255, - designedMinStep: 1, - unit: "%" - }] - }]; + var lightbulbService = new Service.Lightbulb(); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + + lightbulbService + .addCharacteristic(new Characteristic.Brightness()) + .on('set', this.setBrightness.bind(this)); + + return [lightbulbService]; } } From 025bca7a4357c1f516305fe77fc96e30090198e4 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:16:34 -0400 Subject: [PATCH 09/23] factor things out of the accessory and make it a Light --- platforms/HomeAssistant.js | 91 ++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 727cc77..a081adf 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -20,7 +20,8 @@ // 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 types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var url = require('url') var request = require("request"); @@ -68,6 +69,27 @@ HomeAssistantPlatform.prototype = { }) }, + 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."); @@ -82,7 +104,7 @@ HomeAssistantPlatform.prototype = { entity = data[i] if (entity.entity_id.match(lightsRE)) { - accessory = new HomeAssistantAccessory(that.log, entity, that) + accessory = new HomeAssistantLight(that.log, entity, that) foundAccessories.push(accessory) } } @@ -93,8 +115,9 @@ HomeAssistantPlatform.prototype = { } } -function HomeAssistantAccessory(log, data, client) { +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) { @@ -107,43 +130,22 @@ function HomeAssistantAccessory(log, data, client) { this.log = log; } -HomeAssistantAccessory.prototype = { - _callService: function(service, service_data, callback){ - var options = {} - options.body = service_data - - this.client._request('POST', '/services/light/' + service, options, function(error, response, data){ - if (error) { - callback(null) - }else{ - callback(data) - } - }) - }, - _fetchState: function(callback){ - this.client._request('GET', '/states/' + this.entity_id, {}, function(error, response, data){ - if (error) { - callback(null) - }else{ - callback(data) - } - }) - }, +HomeAssistantLight.prototype = { getPowerState: function(callback){ this.log("fetching power state for: " + this.name); - this._fetchState(function(data){ + this.client.fetchState(this.entity_id, function(data){ if (data) { powerState = data.state == 'on' callback(powerState) }else{ callback(null) } - }) + }.bind(this)) }, getBrightness: function(callback){ this.log("fetching brightness for: " + this.name); that = this - this._fetchState(function(data){ + this.client.fetchState(this.entity_id, function(data){ if (data && data.attributes) { that.log('returned brightness: ' + data.attributes.brightness) brightness = ((data.attributes.brightness || 0) / 255)*100 @@ -151,9 +153,9 @@ HomeAssistantAccessory.prototype = { }else{ callback(null) } - }) + }.bind(this)) }, - setPowerState: function(powerOn) { + setPowerState: function(powerOn, callback) { var that = this; var service_data = {} service_data.entity_id = this.entity_id @@ -161,22 +163,28 @@ HomeAssistantAccessory.prototype = { if (powerOn) { this.log("Setting power state on the '"+this.name+"' to on"); - this._callService('turn_on', service_data, function(data){ + 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(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) }else{ this.log("Setting power state on the '"+this.name+"' to off"); - this._callService('turn_off', service_data, function(data){ + 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(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) } }, - setBrightness: function(level) { + setBrightness: function(level, callback) { var that = this; var service_data = {} service_data.entity_id = this.entity_id @@ -185,21 +193,26 @@ HomeAssistantAccessory.prototype = { this.log("Setting brightness on the '"+this.name+"' to " + level); - this._callService('turn_on', service_data, function(data){ + 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(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) }, getServices: function() { var lightbulbService = new Service.Lightbulb(); lightbulbService .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) .on('set', this.setPowerState.bind(this)); lightbulbService - .addCharacteristic(new Characteristic.Brightness()) + .addCharacteristic(Characteristic.Brightness) + .on('get', this.getBrightness.bind(this)) .on('set', this.setBrightness.bind(this)); return [lightbulbService]; @@ -207,5 +220,5 @@ HomeAssistantAccessory.prototype = { } -module.exports.accessory = HomeAssistantAccessory; +module.exports.accessory = HomeAssistantLight; module.exports.platform = HomeAssistantPlatform; From 488456c1081d688e55220337baaf8570ae9369c2 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:30:55 -0400 Subject: [PATCH 10/23] ohhhhhhh the callback signature --- platforms/HomeAssistant.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index a081adf..f08ddfb 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -25,6 +25,8 @@ 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 @@ -133,25 +135,25 @@ function HomeAssistantLight(log, data, client) { 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(powerState) + callback(null, powerState) }else{ - callback(null) + callback(communicationError) } }.bind(this)) }, getBrightness: function(callback){ this.log("fetching brightness for: " + this.name); - that = this + this.client.fetchState(this.entity_id, function(data){ if (data && data.attributes) { - that.log('returned brightness: ' + data.attributes.brightness) brightness = ((data.attributes.brightness || 0) / 255)*100 - callback(brightness) + callback(null, brightness) }else{ - callback(null) + callback(communicationError) } }.bind(this)) }, @@ -168,7 +170,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set power state on the '"+that.name+"' to on"); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) }else{ @@ -179,7 +181,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set power state on the '"+that.name+"' to off"); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) } @@ -198,7 +200,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set brightness on the '"+that.name+"' to " + level); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) }, From 69d948e0fae82393a409427370d991b3bb941d7c Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:40:03 -0400 Subject: [PATCH 11/23] add switch --- platforms/HomeAssistant.js | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index f08ddfb..6440728 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -222,5 +222,76 @@ HomeAssistantLight.prototype = { } +function HomeAssistantSwitch(log, data, client) { + // device info + this.domain = "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(); + + switchService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + return [switchService]; + } + +} + module.exports.accessory = HomeAssistantLight; +module.exports.accessory = HomeAssistantSwitch; module.exports.platform = HomeAssistantPlatform; From c1e3d45fa1755c838545ba8db409a4f6f1f18a81 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:40:09 -0400 Subject: [PATCH 12/23] scan in switches --- platforms/HomeAssistant.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 6440728..d9984c8 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -98,15 +98,22 @@ HomeAssistantPlatform.prototype = { var that = this; var foundAccessories = []; var lightsRE = /^light\./i + var switchRE = /^switch\./i this._request('GET', '/states', {}, function(error, response, data){ for (var i = 0; i < data.length; i++) { entity = data[i] + var accessory = null if (entity.entity_id.match(lightsRE)) { accessory = new HomeAssistantLight(that.log, entity, that) + }else if (entity.entity_id.match(switchRE)){ + accessory = new HomeAssistantSwitch(that.log, entity, that) + } + + if (accessory) { foundAccessories.push(accessory) } } From 13347d1879df1c9ba7adcb769f204cce914410e5 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:43:29 -0700 Subject: [PATCH 13/23] Upgrade Lockitron accessory to new API --- accessories/Lockitron.js | 244 ++++++++++----------------------------- 1 file changed, 60 insertions(+), 184 deletions(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index b5fd023..130a6bc 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -1,196 +1,72 @@ -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.message, body); + callback(err); + } + }.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]; +} From db3f32c57792634a61c17291a6ddc70a944dbae4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:45:29 -0700 Subject: [PATCH 14/23] Fix name from config --- accessories/Lockitron.js | 1 + 1 file changed, 1 insertion(+) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 130a6bc..32d900a 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -8,6 +8,7 @@ module.exports = { function LockitronAccessory(log, config) { this.log = log; + this.name = config["name"]; this.accessToken = config["api_token"]; this.lockID = config["lock_id"]; } From bb39f5f73e1dfec74c0fabf6d9611051b17c26d9 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:47:21 -0700 Subject: [PATCH 15/23] [Lockitron] err could be null --- accessories/Lockitron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 32d900a..7e7db38 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -50,7 +50,7 @@ LockitronAccessory.prototype.setState = function(state, callback) { callback(null); // success } else { - this.log("Error '%s' setting lock state. Response: %s", err.message, body); + this.log("Error '%s' setting lock state. Response: %s", err, body); callback(err); } }.bind(this)); From adbe116a5ab0c7159b6f5dcd662ea4c48f948ae4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:48:42 -0700 Subject: [PATCH 16/23] [Lockitron] Create an Error if necessary --- accessories/Lockitron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 7e7db38..04414af 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -51,7 +51,7 @@ LockitronAccessory.prototype.setState = function(state, callback) { } else { this.log("Error '%s' setting lock state. Response: %s", err, body); - callback(err); + callback(err || new Error("Error setting lock state.")); } }.bind(this)); }, From c88d01c9a9ebcd7fda45c2615339e6e41e84f0af Mon Sep 17 00:00:00 2001 From: Luke Redpath Date: Mon, 14 Sep 2015 19:29:02 +0100 Subject: [PATCH 17/23] Updated CHANGELOG with my previous changes --- platforms/Domoticz.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index de5b466..2042094 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 @@ -362,4 +365,4 @@ DomoticzAccessory.prototype = { }; module.exports.accessory = DomoticzAccessory; -module.exports.platform = DomoticzPlatform; \ No newline at end of file +module.exports.platform = DomoticzPlatform; From 7f14df04342b32e94bfe710323671456bf754dfa Mon Sep 17 00:00:00 2001 From: Luke Redpath Date: Mon, 14 Sep 2015 19:40:04 +0100 Subject: [PATCH 18/23] Honor the MaxDimLevel property of accessories. LightwaveRF lights could not be dimmed properly as they require a dim level of betwene 0-32. The Domoticz documentation incorrectly states this should be 0-16. Other devices may use different values which is why the MaxDimLevel property should be used rather than hardcoded value. Also refactored the code slightly. --- platforms/Domoticz.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index 2042094..8930011 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -20,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 // @@ -186,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) { @@ -211,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 [ { From e894b1a1a1314c3005d9f8235e9f833f05325c8e Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:14:27 -0400 Subject: [PATCH 19/23] support media players to be addressable as lights --- platforms/HomeAssistant.js | 143 +++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index d9984c8..22a3d72 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -99,6 +99,7 @@ HomeAssistantPlatform.prototype = { var foundAccessories = []; var lightsRE = /^light\./i var switchRE = /^switch\./i + var mediaPlayerRE = /^media_player\./i this._request('GET', '/states', {}, function(error, response, data){ @@ -111,6 +112,8 @@ HomeAssistantPlatform.prototype = { accessory = new HomeAssistantLight(that.log, entity, that) }else if (entity.entity_id.match(switchRE)){ accessory = new HomeAssistantSwitch(that.log, entity, that) + }else if (entity.entity_id.match(mediaPlayerRE) && entity.attributes && entity.attributes.supported_media_commands){ + accessory = new HomeAssistantMediaPlayer(that.log, entity, that) } if (accessory) { @@ -229,6 +232,145 @@ HomeAssistantLight.prototype = { } +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(); + + 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 [lightbulbService]; + } + +} + + function HomeAssistantSwitch(log, data, client) { // device info this.domain = "switch" @@ -300,5 +442,6 @@ HomeAssistantSwitch.prototype = { } module.exports.accessory = HomeAssistantLight; +module.exports.accessory = HomeAssistantMediaPlayer; module.exports.accessory = HomeAssistantSwitch; module.exports.platform = HomeAssistantPlatform; From 1df32fca3d0d9cdc8d852865119d91e37b97c218 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:20:26 -0400 Subject: [PATCH 20/23] add some docs --- platforms/HomeAssistant.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 22a3d72..075d90c 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -7,6 +7,29 @@ // URL: http://home-assistant.io // GitHub: https://github.com/balloob/home-assistant // +// HA accessories supported: Lights, Switches, Media Players. +// +// 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% +// Set the brightness of the Kitchen Speaker to 40% +// // Remember to add platform to config.json. Example: // "platforms": [ // { From be589d4fb50a2e09aed8aa2b8b4a6cc18b26f611 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:46:51 -0400 Subject: [PATCH 21/23] you can do this too --- platforms/HomeAssistant.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 075d90c..6088b94 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -27,7 +27,8 @@ // // Examples // -// Dim the Kitchen Speaker to 40% +// 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: From 13d1ed75cf889b8d5e4ace9ab9c1ea2a3da76000 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 15 Sep 2015 06:55:53 +0200 Subject: [PATCH 22/23] File-based motion or contact sensor This module creates a motion sensor accessory based on watching a directory or file. --- accessories/FileSensor.js | 76 +++++++++++++++++++++++++++++++++++++++ config-sample.json | 9 +++++ package.json | 1 + 3 files changed, 86 insertions(+) create mode 100644 accessories/FileSensor.js 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/config-sample.json b/config-sample.json index 48c1db4..86782e6 100644 --- a/config-sample.json +++ b/config-sample.json @@ -196,6 +196,15 @@ "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 } + ] } diff --git a/package.json b/package.json index 49294ef..f83cf26 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "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", From d6b3fc766793dc42299a4c6e5df73d8da14ae7b4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Tue, 15 Sep 2015 10:58:25 -0700 Subject: [PATCH 23/23] Bump HAP-NodeJS with fixes for Node 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f83cf26..35c8f39 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "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#98ef550c8d6fd961741673d4b695a74dd0126eba", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1",