diff --git a/README.md b/README.md index 55f4e85..1e35d10 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Since Siri supports devices added through HomeKit, this means that with HomeBrid * _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)) + * _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/config-sample.json b/config-sample.json index 201c8f1..e03a94b 100644 --- a/config-sample.json +++ b/config-sample.json @@ -42,6 +42,10 @@ "port": "8000", "username": "username", "password": "password" + }, + { + "accessory": "LogitechHarmony", + "name": "Living Room Harmony Hub" } ], diff --git a/package.json b/package.json index da1e577..561cdab 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,21 @@ }, "license": "ISC", "dependencies": { - "hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c", "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", - "request": "2.49.x", - "node-persist": "0.0.x", - "xmldoc": "0.1.x", - "node-hue-api": "^1.0.5", - "xml2js": "0.4.x", "carwingsjs": "0.0.x", + "elkington": "kevinohara80/elkington", + "hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c", + "harmonyhubjs-client": "^1.1.4", + "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", + "node-hue-api": "^1.0.5", + "node-milight-promise": "0.0.2", + "node-persist": "0.0.x", + "request": "2.49.x", "sonos": "0.8.x", + "telldus-live": "0.2.x", "wemo": "0.2.x", "wink-js": "0.0.5", - "elkington": "kevinohara80/elkington", - "node-milight-promise": "0.0.2", - "telldus-live" : "0.2.x" + "xml2js": "0.4.x", + "xmldoc": "0.1.x" } } diff --git a/platforms/LogitechHarmony.js b/platforms/LogitechHarmony.js new file mode 100644 index 0000000..a5c5e22 --- /dev/null +++ b/platforms/LogitechHarmony.js @@ -0,0 +1,305 @@ +'use strict'; + +// Logitech Harmony Remote Platform Shim for HomeBridge +// Based on the Domoticz Platform Shim for HomeBridge by Joep Verhaeg (http://www.joepverhaeg.nl) +// Wriiten by John Wells (https://github.com/madmod) +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "LogitechHarmony", +// "name": "Logitech Harmony" +// } +// ], +// +// 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 harmonyDiscover = require('harmonyhubjs-discover'); +var harmony = require('harmonyhubjs-client'); + +var _harmonyHubPort = 61991; + + +function sortByKey (array, key) { + return array.sort(function(a, b) { + var x = a[key]; var y = b[key]; + return ((x < y) ? -1 : ((x > y) ? 1 : 0)); + }); +}; + + +function LogitechHarmonyPlatform (log, config) { + this.log = log; + this.ip_address = config['ip_address']; +}; + + +LogitechHarmonyPlatform.prototype = { + + // Find one Harmony remote hub (only support one for now) + locateHub: function (callback) { + var self = this; + + // Connect to a Harmony hub + var createClient = function (ipAddress) { + self.log("Connecting to Logitech Harmony remote hub..."); + + harmony(ipAddress) + .then(function (client) { + self.log("Connected to Logitech Harmony remote hub"); + + callback(null, client); + }); + }; + + // Use the ip address in configuration if available + if (this.ip_address) { + console.log("Using Logitech Harmony hub ip address from configuration"); + + return createClient(this.ip_address) + } + + this.log("Searching for Logitech Harmony remote hubs..."); + + // Discover the harmony hub with bonjour + var discover = new harmonyDiscover(_harmonyHubPort); + + // TODO: Support update event with some way to add accessories + // TODO: Have some kind of timeout with an error message. Right now this searches forever until it finds one hub. + discover.on('online', function (hubInfo) { + self.log("Found Logitech Harmony remote hub: " + hubInfo.ip); + + // Stop looking for hubs once we find the first one + // TODO: Support multiple hubs + discover.stop(); + + createClient(hubInfo.ip); + }); + + // Start looking for hubs + discover.start(); + }, + + accessories: function (callback) { + var self = this; + var foundAccessories = []; + + // Get the first hub + this.locateHub(function (err, hub) { + if (err) throw err; + + self.log("Fetching Logitech Harmony devices and activites..."); + + //getDevices(hub); + getActivities(hub); + }); + + // Get Harmony Devices + /* + var getDevices = function(hub) { + self.log("Fetching Logitech Harmony devices..."); + + hub.getDevices() + .then(function (devices) { + self.log("Found devices: ", devices); + + var sArray = sortByKey(json['result'],"Name"); + + sArray.map(function(s) { + accessory = new LogitechHarmonyAccessory(self.log, self.server, self.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW")); + foundAccessories.push(accessory); + }); + + callback(foundAccessories); + }); + }; + */ + + // Get Harmony Activities + var getActivities = function(hub) { + self.log("Fetching Logitech Harmony activities..."); + + hub.getActivities() + .then(function (activities) { + self.log("Found activities: \n" + activities.map(function (a) { return "\t" + a.label; }).join("\n")); + + var sArray = sortByKey(activities, "label"); + + sArray.map(function(s) { + var accessory = new LogitechHarmonyAccessory(self.log, hub, s, true); + // TODO: Update the initial power state + foundAccessories.push(accessory); + }); + + callback(foundAccessories); + }); + }; + + } + +}; + + +function LogitechHarmonyAccessory (log, hub, details, isActivity) { + this.log = log; + this.hub = hub; + this.details = details; + this.id = details.id; + this.name = details.label; + this.isActivity = isActivity; + this.isActivityActive = false; +}; + + +LogitechHarmonyAccessory.prototype = { + + // TODO: Somehow make this event driven so that it tells the user what activity is on + getPowerState: function (callback) { + var self = this; + + if (this.isActivity) { + hub.getCurrentActivity().then(function (currentActivity) { + callback(currentActivity.id === self.id); + }).except(function (err) { + self.log('Unable to get current activity with error', err); + callback(false); + }); + } else { + // TODO: Support onRead for devices + this.log('TODO: Support onRead for devices'); + } + }, + + setPowerState: function (state, callback) { + var self = this; + + if (this.isActivity) { + this.log('Set activity ' + this.name + ' power state to ' + state); + + // Activity id -1 is turn off all devices + var id = state ? this.id : -1; + + this.hub.startActivity(id) + .then(function () { + self.log('Finished setting activity ' + self.name + ' power state to ' + state); + callback(); + }) + .catch(function (err) { + self.log('Failed setting activity ' + self.name + ' power state to ' + state + ' with error ' + err); + callback(err); + }); + } else { + // TODO: Support setting device power + this.log('TODO: Support setting device power'); + callback(); + } + }, + + getServices: function () { + var self = this; + + return [ + { + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: self.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + }, + { + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Logitech", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + }, + { + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Harmony", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + }, + { + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + // TODO: Add hub unique id to this for people with multiple hubs so that it is really a guid. + initialValue: self.id, + 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.SWITCH_STYPE, + characteristics: [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: self.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }, + { + cType: types.POWER_STATE_CTYPE, + onUpdate: function (value) { + self.setPowerState(value) + }, + onRead: self.getPowerState, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + } + ] + } + ]; + } + +}; + +module.exports.accessory = LogitechHarmonyAccessory; +module.exports.platform = LogitechHarmonyPlatform; +